- handle errors of source service runs
[opensuse:osc.git] / osc / core.py
1 # Copyright (C) 2006 Novell Inc.  All rights reserved.
2 # This program is free software; it may be used, copied, modified
3 # and distributed under the terms of the GNU General Public Licence,
4 # either version 2, or version 3 (at your option).
5
6 __version__ = '0.131git'
7
8 # __store_version__ is to be incremented when the format of the working copy
9 # "store" changes in an incompatible way. Please add any needed migration
10 # functionality to check_store_version().
11 __store_version__ = '1.0'
12
13 import os
14 import os.path
15 import sys
16 import urllib2
17 from urllib import pathname2url, quote_plus, urlencode, unquote
18 from urlparse import urlsplit, urlunsplit
19 from cStringIO import StringIO
20 import shutil
21 import oscerr
22 import conf
23 import subprocess
24 import re
25 import socket
26 try:
27     from xml.etree import cElementTree as ET
28 except ImportError:
29     import cElementTree as ET
30
31
32
33 DISTURL_RE = re.compile(r"^(?P<bs>.*)://(?P<apiurl>.*?)/(?P<project>.*?)/(?P<repository>.*?)/(?P<revision>.*)-(?P<source>.*)$")
34 BUILDLOGURL_RE = re.compile(r"^(?P<apiurl>https?://.*?)/build/(?P<project>.*?)/(?P<repository>.*?)/(?P<arch>.*?)/(?P<package>.*?)/_log$")
35 BUFSIZE = 1024*1024
36 store = '.osc'
37
38 new_project_templ = """\
39 <project name="%(name)s">
40
41   <title></title> <!-- Short title of NewProject -->
42   <description>
43     <!-- This is for a longer description of the purpose of the project -->
44   </description>
45
46   <person role="maintainer" userid="%(user)s" />
47   <person role="bugowner" userid="%(user)s" />
48 <!-- remove this block to publish your packages on the mirrors -->
49   <publish>
50     <disable />
51   </publish>
52   <build>
53     <enable />
54   </build>
55   <debuginfo>
56     <disable />
57   </debuginfo>
58
59 <!-- remove this comment to enable one or more build targets
60
61   <repository name="openSUSE_Factory">
62     <path project="openSUSE:Factory" repository="standard" />
63     <arch>x86_64</arch>
64     <arch>i586</arch>
65   </repository>
66   <repository name="openSUSE_11.2">
67     <path project="openSUSE:11.2" repository="standard"/>
68     <arch>x86_64</arch>
69     <arch>i586</arch>
70   </repository>
71   <repository name="openSUSE_11.1">
72     <path project="openSUSE:11.1" repository="standard"/>
73     <arch>x86_64</arch>
74     <arch>i586</arch>
75   </repository>
76   <repository name="Fedora_12">
77     <path project="Fedora:12" repository="standard" />
78     <arch>x86_64</arch>
79     <arch>i586</arch>
80   </repository>
81   <repository name="SLE_11">
82     <path project="SUSE:SLE-11" repository="standard" />
83     <arch>x86_64</arch>
84     <arch>i586</arch>
85   </repository>
86 -->
87
88 </project>
89 """
90
91 new_package_templ = """\
92 <package name="%(name)s">
93
94   <title></title> <!-- Title of package -->
95
96   <description>
97 <!-- for long description -->
98   </description>
99
100 <!-- following roles are inherited from the parent project
101   <person role="maintainer" userid="%(user)s"/>
102   <person role="bugowner" userid="%(user)s"/>
103 -->
104 <!--
105   <url>PUT_UPSTREAM_URL_HERE</url>
106 -->
107
108 <!--
109   use one of the examples below to disable building of this package
110   on a certain architecture, in a certain repository,
111   or a combination thereof:
112
113   <disable arch="x86_64"/>
114   <disable repository="SUSE_SLE-10"/>
115   <disable repository="SUSE_SLE-10" arch="x86_64"/>
116
117   Possible sections where you can use the tags above:
118   <build>
119   </build>
120   <debuginfo>
121   </debuginfo>
122   <publish>
123   </publish>
124   <useforbuild>
125   </useforbuild>
126
127   Please have a look at:
128   http://en.opensuse.org/Restricted_formats
129   Packages containing formats listed there are NOT allowed to
130   be packaged in the openSUSE Buildservice and will be deleted!
131
132 -->
133
134 </package>
135 """
136
137 new_attribute_templ = """\
138 <attributes>
139   <attribute namespace="" name="">
140     <value><value>
141   </attribute>
142 </attributes>
143 """
144
145 new_user_template = """\
146 <person>
147   <login>%(user)s</login>
148   <email>PUT_EMAIL_ADDRESS_HERE</email>
149   <realname>PUT_REAL_NAME_HERE</realname>
150   <watchlist>
151     <project name="home:%(user)s"/>
152   </watchlist>
153 </person>
154 """
155
156 info_templ = """\
157 Project name: %s
158 Package name: %s
159 Path: %s
160 API URL: %s
161 Source URL: %s
162 srcmd5: %s
163 Revision: %s
164 Link info: %s
165 """
166
167 new_pattern_template = """\
168 <!-- See http://svn.opensuse.org/svn/zypp/trunk/libzypp/zypp/parser/yum/schema/patterns.rng -->
169
170 <pattern>
171 </pattern>
172 """
173
174 buildstatus_symbols = {'succeeded':       '.',
175                        'disabled':        ' ',
176                        'expansion error': 'U',  # obsolete with OBS 2.0
177                        'unresolvable':    'U',
178                        'failed':          'F',
179                        'broken':          'B',
180                        'blocked':         'b',
181                        'building':        '%',
182                        'finished':        'f',
183                        'scheduled':       's',
184                        'excluded':        'x',
185                        'dispatching':     'd',
186                        'signing':         'S',
187 }
188
189
190 # os.path.samefile is available only under Unix
191 def os_path_samefile(path1, path2):
192     try:
193         return os.path.samefile(path1, path2)
194     except:
195         return os.path.realpath(path1) == os.path.realpath(path2)
196
197 class File:
198     """represent a file, including its metadata"""
199     def __init__(self, name, md5, size, mtime, skipped=False):
200         self.name = name
201         self.md5 = md5
202         self.size = size
203         self.mtime = mtime
204         self.skipped = skipped
205     def __repr__(self):
206         return self.name
207     def __str__(self):
208         return self.name
209
210
211 class Serviceinfo:
212     """Source service content
213     """
214     def __init__(self):
215         """creates an empty serviceinfo instance"""
216         self.services = None
217
218     def read(self, serviceinfo_node, append=False):
219         """read in the source services <services> element passed as
220         elementtree node.
221         """
222         if serviceinfo_node == None:
223             return
224         if not append or self.services == None:
225             self.services = []
226         services = serviceinfo_node.findall('service')
227
228         for service in services:
229             name = service.get('name')
230             mode = service.get('mode', None)
231             data = { 'name' : name, 'mode' : '' }
232             if mode:
233                 data['mode'] = mode
234             try:
235                 for param in service.findall('param'):
236                     option = param.get('name', None)
237                     value = param.text
238                     name += " --" + option + " '" + value + "'"
239                 data['command'] = name
240                 self.services.append(data)
241             except:
242                 msg = 'invalid service format:\n%s' % ET.tostring(serviceinfo_node)
243                 raise oscerr.APIError(msg)
244
245     def getProjectGlobalServices(self, apiurl, project, package):
246         # get all project wide services in one file, we don't store it yet
247         u = makeurl(apiurl, ['source', project, package], query='cmd=getprojectservices')
248         try:
249             f = http_POST(u)
250             root = ET.parse(f).getroot()
251             self.read(root, True)
252         except urllib2.HTTPError, e:
253             if e.code != 404:
254                 raise e
255
256     def addVerifyFile(self, serviceinfo_node, filename):
257         import hashlib
258
259         f = open(filename, 'r')
260         digest = hashlib.sha256(f.read()).hexdigest()
261         f.close()
262
263         r = serviceinfo_node
264         s = ET.Element( "service", name="verify_file" )
265         ET.SubElement(s, "param", name="file").text = filename
266         ET.SubElement(s, "param", name="verifier").text  = "sha256"
267         ET.SubElement(s, "param", name="checksum").text = digest
268
269         r.append( s )
270         return r
271
272
273     def addDownloadUrl(self, serviceinfo_node, url_string):
274         from urlparse import urlparse
275         url = urlparse( url_string )
276         protocol = url.scheme
277         host = url.netloc
278         path = url.path
279
280         r = serviceinfo_node
281         s = ET.Element( "service", name="download_url" )
282         ET.SubElement(s, "param", name="protocol").text = protocol
283         ET.SubElement(s, "param", name="host").text     = host
284         ET.SubElement(s, "param", name="path").text     = path
285
286         r.append( s )
287         return r
288
289     def addGitUrl(self, serviceinfo_node, url_string):
290         r = serviceinfo_node
291         s = ET.Element( "service", name="tar_scm" )
292         ET.SubElement(s, "param", name="url").text = url_string
293         ET.SubElement(s, "param", name="scm").text = "git"
294         r.append( s )
295         return r
296
297     def addRecompressTar(self, serviceinfo_node):
298         r = serviceinfo_node
299         s = ET.Element( "service", name="recompress" )
300         ET.SubElement(s, "param", name="file").text = "*.tar"
301         ET.SubElement(s, "param", name="compression").text = "bz2"
302         r.append( s )
303         return r
304
305     def execute(self, dir, callmode = None, singleservice = None):
306         import tempfile
307
308         # cleanup existing generated files
309         for filename in os.listdir(dir):
310             if filename.startswith('_service:') or filename.startswith('_service_'):
311                 os.unlink(os.path.join(dir, filename))
312
313         allservices = self.services or []
314         if singleservice and not singleservice in allservices:
315             # set array to the manual specified singleservice, if it is not part of _service file
316             data = { 'name' : singleservice, 'command' : singleservice, 'mode' : '' }
317             allservices = [data]
318
319         # recreate files
320         ret = 0
321         for service in allservices:
322             if singleservice and service['name'] != singleservice:
323                 continue
324             if service['mode'] == "disabled" and callmode != "disabled":
325                 continue
326             if service['mode'] != "disabled" and callmode == "disabled":
327                 continue
328             call = service['command']
329             temp_dir = tempfile.mkdtemp()
330             name = call.split(None, 1)[0]
331             if not os.path.exists("/usr/lib/obs/service/"+name):
332                 raise oscerr.PackageNotInstalled("obs-service-"+name)
333             c = "/usr/lib/obs/service/" + call + " --outdir " + temp_dir
334             if conf.config['verbose'] > 1:
335                 print "Run source service:", c
336             r = subprocess.call(c, shell=True)
337             if r != 0:
338                 print "ERROR: service call failed: " + c
339                 # FIXME: addDownloadUrlService calls si.execute after 
340                 #        updating _services.
341                 print "       (your _services file may be corrupt now)"
342                 ret = r
343
344             if service['mode'] == "disabled" or service['mode'] == "trylocal" or service['mode'] == "localonly" or callmode == "local":
345                 for filename in os.listdir(temp_dir):
346                     shutil.move( os.path.join(temp_dir, filename), os.path.join(dir, filename) )
347             else:
348                 for filename in os.listdir(temp_dir):
349                     shutil.move( os.path.join(temp_dir, filename), os.path.join(dir, "_service:"+name+":"+filename) )
350             os.rmdir(temp_dir)
351
352         return ret
353
354 class Linkinfo:
355     """linkinfo metadata (which is part of the xml representing a directory
356     """
357     def __init__(self):
358         """creates an empty linkinfo instance"""
359         self.project = None
360         self.package = None
361         self.xsrcmd5 = None
362         self.lsrcmd5 = None
363         self.srcmd5 = None
364         self.error = None
365         self.rev = None
366         self.baserev = None
367
368     def read(self, linkinfo_node):
369         """read in the linkinfo metadata from the <linkinfo> element passed as
370         elementtree node.
371         If the passed element is None, the method does nothing.
372         """
373         if linkinfo_node == None:
374             return
375         self.project = linkinfo_node.get('project')
376         self.package = linkinfo_node.get('package')
377         self.xsrcmd5 = linkinfo_node.get('xsrcmd5')
378         self.lsrcmd5 = linkinfo_node.get('lsrcmd5')
379         self.srcmd5  = linkinfo_node.get('srcmd5')
380         self.error   = linkinfo_node.get('error')
381         self.rev     = linkinfo_node.get('rev')
382         self.baserev = linkinfo_node.get('baserev')
383
384     def islink(self):
385         """returns True if the linkinfo is not empty, otherwise False"""
386         if self.xsrcmd5 or self.lsrcmd5:
387             return True
388         return False
389
390     def isexpanded(self):
391         """returns True if the package is an expanded link"""
392         if self.lsrcmd5 and not self.xsrcmd5:
393             return True
394         return False
395
396     def haserror(self):
397         """returns True if the link is in error state (could not be applied)"""
398         if self.error:
399             return True
400         return False
401
402     def __str__(self):
403         """return an informatory string representation"""
404         if self.islink() and not self.isexpanded():
405             return 'project %s, package %s, xsrcmd5 %s, rev %s' \
406                     % (self.project, self.package, self.xsrcmd5, self.rev)
407         elif self.islink() and self.isexpanded():
408             if self.haserror():
409                 return 'broken link to project %s, package %s, srcmd5 %s, lsrcmd5 %s: %s' \
410                         % (self.project, self.package, self.srcmd5, self.lsrcmd5, self.error)
411             else:
412                 return 'expanded link to project %s, package %s, srcmd5 %s, lsrcmd5 %s' \
413                         % (self.project, self.package, self.srcmd5, self.lsrcmd5)
414         else:
415             return 'None'
416
417
418 # http://effbot.org/zone/element-lib.htm#prettyprint
419 def xmlindent(elem, level=0):
420     i = "\n" + level*"  "
421     if len(elem):
422         if not elem.text or not elem.text.strip():
423             elem.text = i + "  "
424         for e in elem:
425             xmlindent(e, level+1)
426             if not e.tail or not e.tail.strip():
427                 e.tail = i + "  "
428         if not e.tail or not e.tail.strip():
429             e.tail = i
430     else:
431         if level and (not elem.tail or not elem.tail.strip()):
432             elem.tail = i
433
434 class Project:
435     """represent a project directory, holding packages"""
436     REQ_STOREFILES = ('_project', '_apiurl')
437     if conf.config['do_package_tracking']:
438         REQ_STOREFILES += ('_packages',)
439     def __init__(self, dir, getPackageList=True, progress_obj=None, wc_check=True):
440         import fnmatch
441         self.dir = dir
442         self.absdir = os.path.abspath(dir)
443         self.progress_obj = progress_obj
444
445         self.name = store_read_project(self.dir)
446         self.apiurl = store_read_apiurl(self.dir, defaulturl=not wc_check)
447
448         dirty_files = []
449         if wc_check:
450             dirty_files = self.wc_check()
451         if dirty_files:
452             msg = 'Your working copy \'%s\' is in an inconsistent state.\n' \
453                 'Please run \'osc repairwc %s\' and check the state\n' \
454                 'of the working copy afterwards (via \'osc status %s\')' % (self.dir, self.dir, self.dir)
455             raise oscerr.WorkingCopyInconsistent(self.name, None, dirty_files, msg)
456
457         if getPackageList:
458             self.pacs_available = meta_get_packagelist(self.apiurl, self.name)
459         else:
460             self.pacs_available = []
461
462         if conf.config['do_package_tracking']:
463             self.pac_root = self.read_packages().getroot()
464             self.pacs_have = [ pac.get('name') for pac in self.pac_root.findall('package') ]
465             self.pacs_excluded = [ i for i in os.listdir(self.dir)
466                                    for j in conf.config['exclude_glob']
467                                    if fnmatch.fnmatch(i, j) ]
468             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 ]
469             # store all broken packages (e.g. packages which where removed by a non-osc cmd)
470             # in the self.pacs_broken list
471             self.pacs_broken = []
472             for p in self.pacs_have:
473                 if not os.path.isdir(os.path.join(self.absdir, p)):
474                     # all states will be replaced with the '!'-state
475                     # (except it is already marked as deleted ('D'-state))
476                     self.pacs_broken.append(p)
477         else:
478             self.pacs_have = [ i for i in os.listdir(self.dir) if i in self.pacs_available ]
479
480         self.pacs_missing = [ i for i in self.pacs_available if i not in self.pacs_have ]
481
482     def wc_check(self):
483         global store
484         dirty_files = []
485         for fname in Project.REQ_STOREFILES:
486             if not os.path.exists(os.path.join(self.absdir, store, fname)):
487                 dirty_files.append(fname)
488         return dirty_files
489
490     def wc_repair(self, apiurl=None):
491         global store
492         if not os.path.exists(os.path.join(self.dir, store, '_apiurl')) or apiurl:
493             if apiurl is None:
494                 msg = 'cannot repair wc: the \'_apiurl\' file is missing but ' \
495                     'no \'apiurl\' was passed to wc_repair'
496                 # hmm should we raise oscerr.WrongArgs?
497                 raise oscerr.WorkingCopyInconsistent(self.prjname, self.name, [], msg)
498             # sanity check
499             conf.parse_apisrv_url(None, apiurl)
500             store_write_apiurl(self.dir, apiurl)
501             self.apiurl = store_read_apiurl(self.dir, defaulturl=False)
502
503     def checkout_missing_pacs(self, expand_link=False):
504         for pac in self.pacs_missing:
505
506             if conf.config['do_package_tracking'] and pac in self.pacs_unvers:
507                 # pac is not under version control but a local file/dir exists
508                 msg = 'can\'t add package \'%s\': Object already exists' % pac
509                 raise oscerr.PackageExists(self.name, pac, msg)
510             else:
511                 print 'checking out new package %s' % pac
512                 checkout_package(self.apiurl, self.name, pac, \
513                                  pathname=getTransActPath(os.path.join(self.dir, pac)), \
514                                  prj_obj=self, prj_dir=self.dir, expand_link=expand_link, progress_obj=self.progress_obj)
515
516     def status(self, pac):
517         exists = os.path.exists(os.path.join(self.absdir, pac))
518         st = self.get_state(pac)
519         if st is None and exists:
520             return '?'
521         elif st is None:
522             raise oscerr.OscIOError(None, 'osc: \'%s\' is not under version control' % pac)
523         elif st in ('A', ' ') and not exists:
524             return '!'
525         elif st == 'D' and not exists:
526             return 'D'
527         else:
528             return st
529
530     def get_status(self, *exclude_states):
531         res = []
532         for pac in self.pacs_have:
533             st = self.status(pac)
534             if not st in exclude_states:
535                 res.append((st, pac))
536         if not '?' in exclude_states:
537             res.extend([('?', pac) for pac in self.pacs_unvers])
538         return res
539
540     def get_pacobj(self, pac, *pac_args, **pac_kwargs):
541         try:
542             st = self.status(pac)
543             if st in ('?', '!') or st == 'D' and not os.path.exists(os.path.join(self.dir, pac)):
544                 return None
545             return Package(os.path.join(self.dir, pac), *pac_args, **pac_kwargs)
546         except oscerr.OscIOError:
547             return None
548
549     def set_state(self, pac, state):
550         node = self.get_package_node(pac)
551         if node == None:
552             self.new_package_entry(pac, state)
553         else:
554             node.set('state', state)
555
556     def get_package_node(self, pac):
557         for node in self.pac_root.findall('package'):
558             if pac == node.get('name'):
559                 return node
560         return None
561
562     def del_package_node(self, pac):
563         for node in self.pac_root.findall('package'):
564             if pac == node.get('name'):
565                 self.pac_root.remove(node)
566
567     def get_state(self, pac):
568         node = self.get_package_node(pac)
569         if node != None:
570             return node.get('state')
571         else:
572             return None
573
574     def new_package_entry(self, name, state):
575         ET.SubElement(self.pac_root, 'package', name=name, state=state)
576
577     def read_packages(self):
578         global store
579
580         packages_file = os.path.join(self.absdir, store, '_packages')
581         if os.path.isfile(packages_file) and os.path.getsize(packages_file):
582             return ET.parse(packages_file)
583         else:
584             # scan project for existing packages and migrate them
585             cur_pacs = []
586             for data in os.listdir(self.dir):
587                 pac_dir = os.path.join(self.absdir, data)
588                 # we cannot use self.pacs_available because we cannot guarantee that the package list
589                 # was fetched from the server
590                 if data in meta_get_packagelist(self.apiurl, self.name) and is_package_dir(pac_dir) \
591                    and Package(pac_dir).name == data:
592                     cur_pacs.append(ET.Element('package', name=data, state=' '))
593             store_write_initial_packages(self.absdir, self.name, cur_pacs)
594             return ET.parse(os.path.join(self.absdir, store, '_packages'))
595
596     def write_packages(self):
597         xmlindent(self.pac_root)
598         store_write_string(self.absdir, '_packages', ET.tostring(self.pac_root))
599
600     def addPackage(self, pac):
601         import fnmatch
602         for i in conf.config['exclude_glob']:
603             if fnmatch.fnmatch(pac, i):
604                 msg = 'invalid package name: \'%s\' (see \'exclude_glob\' config option)' % pac
605                 raise oscerr.OscIOError(None, msg)
606         state = self.get_state(pac)
607         if state == None or state == 'D':
608             self.new_package_entry(pac, 'A')
609             self.write_packages()
610             # sometimes the new pac doesn't exist in the list because
611             # it would take too much time to update all data structs regularly
612             if pac in self.pacs_unvers:
613                 self.pacs_unvers.remove(pac)
614         else:
615             raise oscerr.PackageExists(self.name, pac, 'package \'%s\' is already under version control' % pac)
616
617     def delPackage(self, pac, force = False):
618         state = self.get_state(pac.name)
619         can_delete = True
620         if state == ' ' or state == 'D':
621             del_files = []
622             for filename in pac.filenamelist + pac.filenamelist_unvers:
623                 filestate = pac.status(filename)
624                 if filestate == 'M' or filestate == 'C' or \
625                    filestate == 'A' or filestate == '?':
626                     can_delete = False
627                 else:
628                     del_files.append(filename)
629             if can_delete or force:
630                 for filename in del_files:
631                     pac.delete_localfile(filename)
632                     if pac.status(filename) != '?':
633                         # this is not really necessary
634                         pac.put_on_deletelist(filename)
635                         print statfrmt('D', getTransActPath(os.path.join(pac.dir, filename)))
636                 print statfrmt('D', getTransActPath(os.path.join(pac.dir, os.pardir, pac.name)))
637                 pac.write_deletelist()
638                 self.set_state(pac.name, 'D')
639                 self.write_packages()
640             else:
641                 print 'package \'%s\' has local modifications (see osc st for details)' % pac.name
642         elif state == 'A':
643             if force:
644                 delete_dir(pac.absdir)
645                 self.del_package_node(pac.name)
646                 self.write_packages()
647                 print statfrmt('D', pac.name)
648             else:
649                 print 'package \'%s\' has local modifications (see osc st for details)' % pac.name
650         elif state == None:
651             print 'package is not under version control'
652         else:
653             print 'unsupported state'
654
655     def update(self, pacs = (), expand_link=False, unexpand_link=False, service_files=False):
656         if len(pacs):
657             for pac in pacs:
658                 Package(os.path.join(self.dir, pac), progress_obj=self.progress_obj).update()
659         else:
660             # we need to make sure that the _packages file will be written (even if an exception
661             # occurs)
662             try:
663                 # update complete project
664                 # packages which no longer exists upstream
665                 upstream_del = [ pac for pac in self.pacs_have if not pac in self.pacs_available and self.get_state(pac) != 'A']
666
667                 for pac in upstream_del:
668                     p = Package(os.path.join(self.dir, pac))
669                     self.delPackage(p, force = True)
670                     delete_storedir(p.storedir)
671                     try:
672                         os.rmdir(pac)
673                     except:
674                         pass
675                     self.pac_root.remove(self.get_package_node(p.name))
676                     self.pacs_have.remove(pac)
677
678                 for pac in self.pacs_have:
679                     state = self.get_state(pac)
680                     if pac in self.pacs_broken:
681                         if self.get_state(pac) != 'A':
682                             checkout_package(self.apiurl, self.name, pac,
683                                              pathname=getTransActPath(os.path.join(self.dir, pac)), prj_obj=self, \
684                                              prj_dir=self.dir, expand_link=not unexpand_link, progress_obj=self.progress_obj)
685                     elif state == ' ':
686                         # do a simple update
687                         p = Package(os.path.join(self.dir, pac), progress_obj=self.progress_obj)
688                         rev = None
689                         if expand_link and p.islink() and not p.isexpanded():
690                             if p.haslinkerror():
691                                 try:
692                                     rev = show_upstream_xsrcmd5(p.apiurl, p.prjname, p.name, revision=p.rev)
693                                 except:
694                                     rev = show_upstream_xsrcmd5(p.apiurl, p.prjname, p.name, revision=p.rev, linkrev="base")
695                                     p.mark_frozen()
696                             else:
697                                 rev = p.linkinfo.xsrcmd5
698                             print 'Expanding to rev', rev
699                         elif unexpand_link and p.islink() and p.isexpanded():
700                             rev = p.linkinfo.lsrcmd5
701                             print 'Unexpanding to rev', rev
702                         elif p.islink() and p.isexpanded():
703                             rev = p.latest_rev()
704                         print 'Updating %s' % p.name
705                         p.update(rev, service_files)
706                         if unexpand_link:
707                             p.unmark_frozen()
708                     elif state == 'D':
709                         # TODO: Package::update has to fixed to behave like svn does
710                         if pac in self.pacs_broken:
711                             checkout_package(self.apiurl, self.name, pac,
712                                              pathname=getTransActPath(os.path.join(self.dir, pac)), prj_obj=self, \
713                                              prj_dir=self.dir, expand_link=expand_link, progress_obj=self.progress_obj)
714                         else:
715                             Package(os.path.join(self.dir, pac), progress_obj=self.progress_obj).update()
716                     elif state == 'A' and pac in self.pacs_available:
717                         # file/dir called pac already exists and is under version control
718                         msg = 'can\'t add package \'%s\': Object already exists' % pac
719                         raise oscerr.PackageExists(self.name, pac, msg)
720                     elif state == 'A':
721                         # do nothing
722                         pass
723                     else:
724                         print 'unexpected state.. package \'%s\'' % pac
725
726                 self.checkout_missing_pacs(expand_link=not unexpand_link)
727             finally:
728                 self.write_packages()
729
730     def validate_pacs(self, validators, verbose_validation=False, *pacs):
731         if len(pacs) == 0:
732             for pac in self.pacs_broken:
733                 if self.get_state(pac) != 'D':
734                     msg = 'validation failed: package \'%s\' is missing' % pac
735                     raise oscerr.PackageMissing(self.name, pac, msg)
736             pacs = self.pacs_have
737         for pac in pacs:
738             if pac in self.pacs_broken and self.get_state(pac) != 'D':
739                 msg = 'validation failed: package \'%s\' is missing' % pac
740                 raise oscerr.PackageMissing(self.name, pac, msg)
741             if os_path_samefile(os.path.join(self.dir, pac), os.getcwd()):
742                 p = Package('.')
743             else:
744                 p = Package(os.path.join(self.dir, pac))
745             p.validate(validators, verbose_validation)
746
747     def commit(self, pacs = (), msg = '', files = {}, validators_dir = None, verbose_validation = False):
748         if len(pacs):
749             try:
750                 for pac in pacs:
751                     todo = []
752                     if files.has_key(pac):
753                         todo = files[pac]
754                     state = self.get_state(pac)
755                     if state == 'A':
756                         self.commitNewPackage(pac, msg, todo, validators_dir=validators_dir, verbose_validation=verbose_validation)
757                     elif state == 'D':
758                         self.commitDelPackage(pac)
759                     elif state == ' ':
760                         # display the correct dir when sending the changes
761                         if os_path_samefile(os.path.join(self.dir, pac), os.getcwd()):
762                             p = Package('.')
763                         else:
764                             p = Package(os.path.join(self.dir, pac))
765                         p.todo = todo
766                         p.commit(msg, validators_dir=validators_dir, verbose_validation=verbose_validation)
767                     elif pac in self.pacs_unvers and not is_package_dir(os.path.join(self.dir, pac)):
768                         print 'osc: \'%s\' is not under version control' % pac
769                     elif pac in self.pacs_broken:
770                         print 'osc: \'%s\' package not found' % pac
771                     elif state == None:
772                         self.commitExtPackage(pac, msg, todo, validators_dir=validators_dir, verbose_validation=verbose_validation)
773             finally:
774                 self.write_packages()
775         else:
776             # if we have packages marked as '!' we cannot commit
777             for pac in self.pacs_broken:
778                 if self.get_state(pac) != 'D':
779                     msg = 'commit failed: package \'%s\' is missing' % pac
780                     raise oscerr.PackageMissing(self.name, pac, msg)
781             try:
782                 for pac in self.pacs_have:
783                     state = self.get_state(pac)
784                     if state == ' ':
785                         # do a simple commit
786                         Package(os.path.join(self.dir, pac)).commit(msg, validators_dir=validators_dir, verbose_validation=verbose_validation)
787                     elif state == 'D':
788                         self.commitDelPackage(pac)
789                     elif state == 'A':
790                         self.commitNewPackage(pac, msg, validators_dir=validators_dir, verbose_validation=verbose_validation)
791             finally:
792                 self.write_packages()
793
794     def commitNewPackage(self, pac, msg = '', files = [], validators_dir = None, verbose_validation = False):
795         """creates and commits a new package if it does not exist on the server"""
796         if pac in self.pacs_available:
797             print 'package \'%s\' already exists' % pac
798         else:
799             user = conf.get_apiurl_usr(self.apiurl)
800             edit_meta(metatype='pkg',
801                       path_args=(quote_plus(self.name), quote_plus(pac)),
802                       template_args=({
803                               'name': pac,
804                               'user': user}),
805                       apiurl=self.apiurl)
806             # display the correct dir when sending the changes
807             olddir = os.getcwd()
808             if os_path_samefile(os.path.join(self.dir, pac), os.curdir):
809                 os.chdir(os.pardir)
810                 p = Package(pac)
811             else:
812                 p = Package(os.path.join(self.dir, pac))
813             p.todo = files
814             print statfrmt('Sending', os.path.normpath(p.dir))
815             p.commit(msg=msg, validators_dir=validators_dir, verbose_validation=verbose_validation)
816             self.set_state(pac, ' ')
817             os.chdir(olddir)
818
819     def commitDelPackage(self, pac):
820         """deletes a package on the server and in the working copy"""
821         try:
822             # display the correct dir when sending the changes
823             if os_path_samefile(os.path.join(self.dir, pac), os.curdir):
824                 pac_dir = pac
825             else:
826                 pac_dir = os.path.join(self.dir, pac)
827             p = Package(os.path.join(self.dir, pac))
828             #print statfrmt('Deleting', os.path.normpath(os.path.join(p.dir, os.pardir, pac)))
829             delete_storedir(p.storedir)
830             try:
831                 os.rmdir(p.dir)
832             except:
833                 pass
834         except OSError:
835             pac_dir = os.path.join(self.dir, pac)
836         #print statfrmt('Deleting', getTransActPath(os.path.join(self.dir, pac)))
837         print statfrmt('Deleting', getTransActPath(pac_dir))
838         delete_package(self.apiurl, self.name, pac)
839         self.del_package_node(pac)
840
841     def commitExtPackage(self, pac, msg, files = [], validators_dir=None, verbose_validation=False):
842         """commits a package from an external project"""
843         if os_path_samefile(os.path.join(self.dir, pac), os.getcwd()):
844             pac_path = '.'
845         else:
846             pac_path = os.path.join(self.dir, pac)
847
848         project = store_read_project(pac_path)
849         package = store_read_package(pac_path)
850         apiurl = store_read_apiurl(pac_path, defaulturl=False)
851         if not meta_exists(metatype='pkg',
852                            path_args=(quote_plus(project), quote_plus(package)),
853                            template_args=None, create_new=False, apiurl=apiurl):
854             user = conf.get_apiurl_usr(self.apiurl)
855             edit_meta(metatype='pkg',
856                       path_args=(quote_plus(project), quote_plus(package)),
857                       template_args=({'name': pac, 'user': user}), apiurl=apiurl)
858         p = Package(pac_path)
859         p.todo = files
860         p.commit(msg=msg, validators_dir=validators_dir, verbose_validation=verbose_validation)
861
862     def __str__(self):
863         r = []
864         r.append('*****************************************************')
865         r.append('Project %s (dir=%s, absdir=%s)' % (self.name, self.dir, self.absdir))
866         r.append('have pacs:\n%s' % ', '.join(self.pacs_have))
867         r.append('missing pacs:\n%s' % ', '.join(self.pacs_missing))
868         r.append('*****************************************************')
869         return '\n'.join(r)
870
871     @staticmethod
872     def init_project(apiurl, dir, project, package_tracking=True, getPackageList=True, progress_obj=None, wc_check=True):
873         global store
874
875         if not os.path.exists(dir):
876             # use makedirs (checkout_no_colon config option might be enabled)
877             os.makedirs(dir)
878         elif not os.path.isdir(dir):
879             raise oscerr.OscIOError(None, 'error: \'%s\' is no directory' % dir)
880         if os.path.exists(os.path.join(dir, store)):
881             raise oscerr.OscIOError(None, 'error: \'%s\' is already an initialized osc working copy' % dir)
882         else:
883             os.mkdir(os.path.join(dir, store))
884
885         store_write_project(dir, project)
886         store_write_apiurl(dir, apiurl)
887         if package_tracking:
888             store_write_initial_packages(dir, project, [])
889         return Project(dir, getPackageList, progress_obj, wc_check)
890
891
892 class Package:
893     """represent a package (its directory) and read/keep/write its metadata"""
894
895     # should _meta be a required file?
896     REQ_STOREFILES = ('_project', '_package', '_apiurl', '_files', '_osclib_version')
897     OPT_STOREFILES = ('_to_be_added', '_to_be_deleted', '_in_conflict', '_in_update',
898         '_in_commit', '_meta', '_meta_mode', '_frozenlink', '_pulled', '_linkrepair',
899         '_size_limit', '_commit_msg')
900
901     def __init__(self, workingdir, progress_obj=None, size_limit=None, wc_check=True):
902         global store
903
904         self.dir = workingdir
905         self.absdir = os.path.abspath(self.dir)
906         self.storedir = os.path.join(self.absdir, store)
907         self.progress_obj = progress_obj
908         self.size_limit = size_limit
909         if size_limit and size_limit == 0:
910             self.size_limit = None
911
912         check_store_version(self.dir)
913
914         self.prjname = store_read_project(self.dir)
915         self.name = store_read_package(self.dir)
916         self.apiurl = store_read_apiurl(self.dir, defaulturl=not wc_check)
917
918         self.update_datastructs()
919         dirty_files = []
920         if wc_check:
921             dirty_files = self.wc_check()
922         if dirty_files:
923             msg = 'Your working copy \'%s\' is in an inconsistent state.\n' \
924                 'Please run \'osc repairwc %s\' (Note this might _remove_\n' \
925                 'files from the .osc/ dir). Please check the state\n' \
926                 'of the working copy afterwards (via \'osc status %s\')' % (self.dir, self.dir, self.dir)
927             raise oscerr.WorkingCopyInconsistent(self.prjname, self.name, dirty_files, msg)
928
929         self.todo = []
930
931     def wc_check(self):
932         dirty_files = []
933         for fname in self.filenamelist:
934             if not os.path.exists(os.path.join(self.storedir, fname)) and not fname in self.skipped:
935                 dirty_files.append(fname)
936         for fname in Package.REQ_STOREFILES:
937             if not os.path.isfile(os.path.join(self.storedir, fname)):
938                 dirty_files.append(fname)
939         for fname in os.listdir(self.storedir):
940             if fname in Package.REQ_STOREFILES or fname in Package.OPT_STOREFILES or \
941                 fname.startswith('_build'):
942                 continue
943             elif fname in self.filenamelist and fname in self.skipped:
944                 dirty_files.append(fname)
945             elif not fname in self.filenamelist:
946                 dirty_files.append(fname)
947         for fname in self.to_be_deleted[:]:
948             if not fname in self.filenamelist:
949                 dirty_files.append(fname)
950         for fname in self.in_conflict[:]:
951             if not fname in self.filenamelist:
952                 dirty_files.append(fname)
953         return dirty_files
954
955     def wc_repair(self, apiurl=None):
956         if not os.path.exists(os.path.join(self.storedir, '_apiurl')) or apiurl:
957             if apiurl is None:
958                 msg = 'cannot repair wc: the \'_apiurl\' file is missing but ' \
959                     'no \'apiurl\' was passed to wc_repair'
960                 # hmm should we raise oscerr.WrongArgs?
961                 raise oscerr.WorkingCopyInconsistent(self.prjname, self.name, [], msg)
962             # sanity check
963             conf.parse_apisrv_url(None, apiurl)
964             store_write_apiurl(self.dir, apiurl)
965             self.apiurl = store_read_apiurl(self.dir, defaulturl=False)
966         # all files which are present in the filelist have to exist in the storedir
967         for f in self.filelist:
968             # XXX: should we also check the md5?
969             if not os.path.exists(os.path.join(self.storedir, f.name)) and not f.name in self.skipped:
970                 # if get_source_file fails we're screwed up...
971                 get_source_file(self.apiurl, self.prjname, self.name, f.name,
972                     targetfilename=os.path.join(self.storedir, f.name), revision=self.rev,
973                     mtime=f.mtime)
974         for fname in os.listdir(self.storedir):
975             if fname in Package.REQ_STOREFILES or fname in Package.OPT_STOREFILES or \
976                 fname.startswith('_build'):
977                 continue
978             elif not fname in self.filenamelist or fname in self.skipped:
979                 # this file does not belong to the storedir so remove it
980                 os.unlink(os.path.join(self.storedir, fname))
981         for fname in self.to_be_deleted[:]:
982             if not fname in self.filenamelist:
983                 self.to_be_deleted.remove(fname)
984                 self.write_deletelist()
985         for fname in self.in_conflict[:]:
986             if not fname in self.filenamelist:
987                 self.in_conflict.remove(fname)
988                 self.write_conflictlist()
989
990     def info(self):
991         source_url = makeurl(self.apiurl, ['source', self.prjname, self.name])
992         r = info_templ % (self.prjname, self.name, self.absdir, self.apiurl, source_url, self.srcmd5, self.rev, self.linkinfo)
993         return r
994
995     def addfile(self, n):
996         if not os.path.exists(os.path.join(self.absdir, n)):
997             raise oscerr.OscIOError(None, 'error: file \'%s\' does not exist' % n)
998         if n in self.to_be_deleted:
999             self.to_be_deleted.remove(n)
1000 #            self.delete_storefile(n)
1001             self.write_deletelist()
1002         elif n in self.filenamelist or n in self.to_be_added:
1003             raise oscerr.PackageFileConflict(self.prjname, self.name, n, 'osc: warning: \'%s\' is already under version control' % n)
1004 #        shutil.copyfile(os.path.join(self.dir, n), os.path.join(self.storedir, n))
1005         if self.dir != '.':
1006             pathname = os.path.join(self.dir, n)
1007         else:
1008             pathname = n
1009         self.to_be_added.append(n)
1010         self.write_addlist()
1011         print statfrmt('A', pathname)
1012
1013     def delete_file(self, n, force=False):
1014         """deletes a file if possible and marks the file as deleted"""
1015         state = '?'
1016         try:
1017             state = self.status(n)
1018         except IOError, ioe:
1019             if not force:
1020                 raise ioe
1021         if state in ['?', 'A', 'M', 'R', 'C'] and not force:
1022             return (False, state)
1023         # special handling for skipped files: if file exists, simply delete it
1024         if state == 'S':
1025             exists = os.path.exists(os.path.join(self.dir, n))
1026             self.delete_localfile(n)
1027             return (exists, 'S')
1028
1029         self.delete_localfile(n)
1030         was_added = n in self.to_be_added
1031         if state in ('A', 'R') or state == '!' and was_added:
1032             self.to_be_added.remove(n)
1033             self.write_addlist()
1034         elif state == 'C':
1035             # don't remove "merge files" (*.r, *.mine...)
1036             # that's why we don't use clear_from_conflictlist
1037             self.in_conflict.remove(n)
1038             self.write_conflictlist()
1039         if not state in ('A', '?') and not (state == '!' and was_added):
1040             self.put_on_deletelist(n)
1041             self.write_deletelist()
1042         return (True, state)
1043
1044     def delete_storefile(self, n):
1045         try: os.unlink(os.path.join(self.storedir, n))
1046         except: pass
1047
1048     def delete_localfile(self, n):
1049         try: os.unlink(os.path.join(self.dir, n))
1050         except: pass
1051
1052     def put_on_deletelist(self, n):
1053         if n not in self.to_be_deleted:
1054             self.to_be_deleted.append(n)
1055
1056     def put_on_conflictlist(self, n):
1057         if n not in self.in_conflict:
1058             self.in_conflict.append(n)
1059
1060     def put_on_addlist(self, n):
1061         if n not in self.to_be_added:
1062             self.to_be_added.append(n)
1063
1064     def clear_from_conflictlist(self, n):
1065         """delete an entry from the file, and remove the file if it would be empty"""
1066         if n in self.in_conflict:
1067
1068             filename = os.path.join(self.dir, n)
1069             storefilename = os.path.join(self.storedir, n)
1070             myfilename = os.path.join(self.dir, n + '.mine')
1071             if self.islinkrepair() or self.ispulled():
1072                 upfilename = os.path.join(self.dir, n + '.new')
1073             else:
1074                 upfilename = os.path.join(self.dir, n + '.r' + self.rev)
1075
1076             try:
1077                 os.unlink(myfilename)
1078                 # the working copy may be updated, so the .r* ending may be obsolete...
1079                 # then we don't care
1080                 os.unlink(upfilename)
1081                 if self.islinkrepair() or self.ispulled():
1082                     os.unlink(os.path.join(self.dir, n + '.old'))
1083             except:
1084                 pass
1085
1086             self.in_conflict.remove(n)
1087
1088             self.write_conflictlist()
1089
1090     # XXX: this isn't used at all
1091     def write_meta_mode(self):
1092         # XXX: the "elif" is somehow a contradiction (with current and the old implementation
1093         #      it's not possible to "leave" the metamode again) (except if you modify pac.meta
1094         #      which is really ugly:) )
1095         if self.meta:
1096             store_write_string(self.absdir, '_meta_mode', '')
1097         elif self.ismetamode():
1098             os.unlink(os.path.join(self.storedir, '_meta_mode'))
1099
1100     def write_sizelimit(self):
1101         if self.size_limit and self.size_limit <= 0:
1102             try:
1103                 os.unlink(os.path.join(self.storedir, '_size_limit'))
1104             except:
1105                 pass
1106         else:
1107             store_write_string(self.absdir, '_size_limit', str(self.size_limit) + '\n')
1108
1109     def write_addlist(self):
1110         self.__write_storelist('_to_be_added', self.to_be_added)
1111
1112     def write_deletelist(self):
1113         self.__write_storelist('_to_be_deleted', self.to_be_deleted)
1114
1115     def delete_source_file(self, n):
1116         """delete local a source file"""
1117         self.delete_localfile(n)
1118         self.delete_storefile(n)
1119
1120     def delete_remote_source_file(self, n):
1121         """delete a remote source file (e.g. from the server)"""
1122         query = 'rev=upload'
1123         u = makeurl(self.apiurl, ['source', self.prjname, self.name, pathname2url(n)], query=query)
1124         http_DELETE(u)
1125
1126     def put_source_file(self, n, copy_only=False):
1127         cdir = os.path.join(self.storedir, '_in_commit')
1128         try:
1129             if not os.path.isdir(cdir):
1130                 os.mkdir(cdir)
1131             query = 'rev=repository'
1132             tmpfile = os.path.join(cdir, n)
1133             shutil.copyfile(os.path.join(self.dir, n), tmpfile)
1134             # escaping '+' in the URL path (note: not in the URL query string) is
1135             # only a workaround for ruby on rails, which swallows it otherwise
1136             if not copy_only:
1137                 u = makeurl(self.apiurl, ['source', self.prjname, self.name, pathname2url(n)], query=query)
1138                 http_PUT(u, file = os.path.join(self.dir, n))
1139             os.rename(tmpfile, os.path.join(self.storedir, n))
1140         finally:
1141             if os.path.isdir(cdir):
1142                 shutil.rmtree(cdir)
1143         if n in self.to_be_added:
1144             self.to_be_added.remove(n)
1145
1146     def __generate_commitlist(self, todo_send):
1147         root = ET.Element('directory')
1148         keys = todo_send.keys()
1149         keys.sort()
1150         for i in keys:
1151             ET.SubElement(root, 'entry', name=i, md5=todo_send[i])
1152         return root
1153
1154     def __send_commitlog(self, msg, local_filelist):
1155         """send the commitlog and the local filelist to the server"""
1156         query = {'cmd'    : 'commitfilelist',
1157                  'user'   : conf.get_apiurl_usr(self.apiurl),
1158                  'comment': msg}
1159         if self.islink() and self.isexpanded():
1160             query['keeplink'] = '1'
1161             if conf.config['linkcontrol'] or self.isfrozen():
1162                 query['linkrev'] = self.linkinfo.srcmd5
1163             if self.ispulled():
1164                 query['repairlink'] = '1'
1165                 query['linkrev'] = self.get_pulled_srcmd5()
1166         if self.islinkrepair():
1167             query['repairlink'] = '1'
1168         u = makeurl(self.apiurl, ['source', self.prjname, self.name], query=query)
1169         f = http_POST(u, data=ET.tostring(local_filelist))
1170         root = ET.parse(f).getroot()
1171         return root
1172
1173     def __get_todo_send(self, server_filelist):
1174         """parse todo from a previous __send_commitlog call"""
1175         error = server_filelist.get('error')
1176         if error is None:
1177             return []
1178         elif error != 'missing':
1179             raise oscerr.PackageInternalError(self.prjname, self.name,
1180                 '__get_todo_send: unexpected \'error\' attr: \'%s\'' % error)
1181         todo = []
1182         for n in server_filelist.findall('entry'):
1183             name = n.get('name')
1184             if name is None:
1185                 raise oscerr.APIError('missing \'name\' attribute:\n%s\n' % ET.tostring(server_filelist))
1186             todo.append(n.get('name'))
1187         return todo
1188
1189     def validate(self, validators_dir, verbose_validation=False):
1190         import subprocess
1191         import stat
1192         if validators_dir is None or self.name.startswith('_'):
1193             return
1194         for validator in sorted(os.listdir(validators_dir)):
1195             if validator.startswith('.'):
1196                 continue
1197             fn = os.path.join(validators_dir, validator)
1198             mode = os.stat(fn).st_mode
1199             if stat.S_ISREG(mode):
1200                 if verbose_validation:
1201                     print 'osc runs source validator: %s' % fn
1202                     p = subprocess.Popen([fn, '--verbose'], close_fds=True)
1203                 else:
1204                     p = subprocess.Popen([fn], close_fds=True)
1205                 if p.wait() != 0:
1206                     raise oscerr.ExtRuntimeError('ERROR: source_validator failed:\n%s' % p.stdout, validator)
1207
1208     def commit(self, msg='', validators_dir=None, verbose_validation=False):
1209         # commit only if the upstream revision is the same as the working copy's
1210         upstream_rev = self.latest_rev()
1211         if self.rev != upstream_rev:
1212             raise oscerr.WorkingCopyOutdated((self.absdir, self.rev, upstream_rev))
1213
1214         if not validators_dir is None:
1215             self.validate(validators_dir, verbose_validation)
1216
1217         if not self.todo:
1218             self.todo = [i for i in self.to_be_added if not i in self.filenamelist] + self.filenamelist
1219
1220         pathn = getTransActPath(self.dir)
1221
1222         todo_send = {}
1223         todo_delete = []
1224         real_send = []
1225         for filename in self.filenamelist + [i for i in self.to_be_added if not i in self.filenamelist]:
1226             if filename.startswith('_service:') or filename.startswith('_service_'):
1227                 continue
1228             st = self.status(filename)
1229             if st == 'C':
1230                 print 'Please resolve all conflicts before committing using "osc resolved FILE"!'
1231                 return 1
1232             elif filename in self.todo:
1233                 if st in ('A', 'R', 'M'):
1234                     todo_send[filename] = dgst(os.path.join(self.absdir, filename))
1235                     real_send.append(filename)
1236                     print statfrmt('Sending', os.path.join(pathn, filename))
1237                 elif st in (' ', '!', 'S'):
1238                     if st == '!' and filename in self.to_be_added:
1239                         print 'file \'%s\' is marked as \'A\' but does not exist' % filename
1240                         return 1
1241                     f = self.findfilebyname(filename)
1242                     if f is None:
1243                         raise oscerr.PackageInternalError(self.prjname, self.name,
1244                             'error: file \'%s\' with state \'%s\' is not known by meta' \
1245                             % (filename, st))
1246                     todo_send[filename] = f.md5
1247                 elif st == 'D':
1248                     todo_delete.append(filename)
1249                     print statfrmt('Deleting', os.path.join(pathn, filename))
1250             elif st in ('R', 'M', 'D', ' ', '!', 'S'):
1251                 # ignore missing new file (it's not part of the current commit)
1252                 if st == '!' and filename in self.to_be_added:
1253                     continue
1254                 f = self.findfilebyname(filename)
1255                 if f is None:
1256                     raise oscerr.PackageInternalError(self.prjname, self.name,
1257                         'error: file \'%s\' with state \'%s\' is not known by meta' \
1258                         % (filename, st))
1259                 todo_send[filename] = f.md5
1260
1261         if not real_send and not todo_delete and not self.islinkrepair() and not self.ispulled():
1262             print 'nothing to do for package %s' % self.name
1263             return 1
1264
1265         print 'Transmitting file data ',
1266         filelist = self.__generate_commitlist(todo_send)
1267         sfilelist = self.__send_commitlog(msg, filelist)
1268         send = self.__get_todo_send(sfilelist)
1269         real_send = [i for i in real_send if not i in send]
1270         # abort after 3 tries
1271         tries = 3
1272         while len(send) and tries:
1273             for filename in send[:]:
1274                 sys.stdout.write('.')
1275                 sys.stdout.flush()
1276                 self.put_source_file(filename)
1277                 send.remove(filename)
1278             tries -= 1
1279             sfilelist = self.__send_commitlog(msg, filelist)
1280             send = self.__get_todo_send(sfilelist)
1281         if len(send):
1282             raise oscerr.PackageInternalError(self.prjname, self.name,
1283                 'server does not accept filelist:\n%s\nmissing:\n%s\n' \
1284                 % (ET.tostring(filelist), ET.tostring(sfilelist)))
1285         # these files already exist on the server
1286         # just copy them into the storedir
1287         for filename in real_send:
1288             self.put_source_file(filename, copy_only=True)
1289
1290         self.rev = sfilelist.get('rev')
1291         print
1292         print 'Committed revision %s.' % self.rev
1293
1294         if self.ispulled():
1295             os.unlink(os.path.join(self.storedir, '_pulled'))
1296         if self.islinkrepair():
1297             os.unlink(os.path.join(self.storedir, '_linkrepair'))
1298             self.linkrepair = False
1299             # XXX: mark package as invalid?
1300             print 'The source link has been repaired. This directory can now be removed.'
1301
1302         if self.islink() and self.isexpanded():
1303             li = Linkinfo()
1304             li.read(sfilelist.find('linkinfo'))
1305             if li.xsrcmd5 is None:
1306                 raise oscerr.APIError('linkinfo has no xsrcmd5 attr:\n%s\n' % ET.tostring(sfilelist))
1307             sfilelist = ET.fromstring(self.get_files_meta(revision=li.xsrcmd5))
1308         for i in sfilelist.findall('entry'):
1309             if i.get('name') in self.skipped:
1310                 i.set('skipped', 'true')
1311         store_write_string(self.absdir, '_files', ET.tostring(sfilelist) + '\n')
1312         for filename in todo_delete:
1313             self.to_be_deleted.remove(filename)
1314             self.delete_storefile(filename)
1315         self.write_deletelist()
1316         self.write_addlist()
1317         self.update_datastructs()
1318
1319         print_request_list(self.apiurl, self.prjname, self.name)
1320
1321         if self.findfilebyname("_service"):
1322             print 'Waiting for server side source service run',
1323             u = makeurl(self.apiurl, ['source', self.prjname, self.name])
1324             while 1:
1325                 f = http_GET(u)
1326                 sfilelist = ET.parse(f).getroot()
1327                 s = sfilelist.find('serviceinfo')
1328                 if s.get('code') == "running":
1329                    print '.',
1330                 else:
1331                    break
1332             self.update()
1333             print " updated"
1334             
1335
1336     def __write_storelist(self, name, data):
1337         if len(data) == 0:
1338             try:
1339                 os.unlink(os.path.join(self.storedir, name))
1340             except:
1341                 pass
1342         else:
1343             store_write_string(self.absdir, name, '%s\n' % '\n'.join(data))
1344
1345     def write_conflictlist(self):
1346         self.__write_storelist('_in_conflict', self.in_conflict)
1347
1348     def updatefile(self, n, revision, mtime=None):
1349         filename = os.path.join(self.dir, n)
1350         storefilename = os.path.join(self.storedir, n)
1351         origfile_tmp = os.path.join(self.storedir, '_in_update', '%s.copy' % n)
1352         origfile = os.path.join(self.storedir, '_in_update', n)
1353         if os.path.isfile(filename):
1354             shutil.copyfile(filename, origfile_tmp)
1355             os.rename(origfile_tmp, origfile)
1356         else:
1357             origfile = None
1358
1359         get_source_file(self.apiurl, self.prjname, self.name, n, targetfilename=storefilename,
1360                 revision=revision, progress_obj=self.progress_obj, mtime=mtime, meta=self.meta)
1361
1362         shutil.copyfile(storefilename, filename)
1363         if not origfile is None:
1364             os.unlink(origfile)
1365
1366     def mergefile(self, n, revision, mtime=None):
1367         filename = os.path.join(self.dir, n)
1368         storefilename = os.path.join(self.storedir, n)
1369         myfilename = os.path.join(self.dir, n + '.mine')
1370         upfilename = os.path.join(self.dir, n + '.r' + self.rev)
1371         origfile_tmp = os.path.join(self.storedir, '_in_update', '%s.copy' % n)
1372         origfile = os.path.join(self.storedir, '_in_update', n)
1373         shutil.copyfile(filename, origfile_tmp)
1374         os.rename(origfile_tmp, origfile)
1375         os.rename(filename, myfilename)
1376
1377         get_source_file(self.apiurl, self.prjname, self.name, n,
1378                         revision=revision, targetfilename=upfilename,
1379                         progress_obj=self.progress_obj, mtime=mtime, meta=self.meta)
1380
1381         if binary_file(myfilename) or binary_file(upfilename):
1382             # don't try merging
1383             shutil.copyfile(upfilename, filename)
1384             shutil.copyfile(upfilename, storefilename)
1385             os.unlink(origfile)
1386             self.in_conflict.append(n)
1387             self.write_conflictlist()
1388             return 'C'
1389         else:
1390             # try merging
1391             # diff3 OPTIONS... MINE OLDER YOURS
1392             merge_cmd = 'diff3 -m -E %s %s %s > %s' % (myfilename, storefilename, upfilename, filename)
1393             # we would rather use the subprocess module, but it is not availablebefore 2.4
1394             ret = subprocess.call(merge_cmd, shell=True)
1395
1396             #   "An exit status of 0 means `diff3' was successful, 1 means some
1397             #   conflicts were found, and 2 means trouble."
1398             if ret == 0:
1399                 # merge was successful... clean up
1400                 shutil.copyfile(upfilename, storefilename)
1401                 os.unlink(upfilename)
1402                 os.unlink(myfilename)
1403                 os.unlink(origfile)
1404                 return 'G'
1405             elif ret == 1:
1406                 # unsuccessful merge
1407                 shutil.copyfile(upfilename, storefilename)
1408                 os.unlink(origfile)
1409                 self.in_conflict.append(n)
1410                 self.write_conflictlist()
1411                 return 'C'
1412             else:
1413                 raise oscerr.ExtRuntimeError('diff3 failed with exit code: %s' % ret, merge_cmd)
1414
1415     def update_local_filesmeta(self, revision=None):
1416         """
1417         Update the local _files file in the store.
1418         It is replaced with the version pulled from upstream.
1419         """
1420         meta = self.get_files_meta(revision=revision)
1421         store_write_string(self.absdir, '_files', meta + '\n')
1422
1423     def get_files_meta(self, revision='latest', skip_service=True):
1424         fm = show_files_meta(self.apiurl, self.prjname, self.name, revision=revision, meta=self.meta)
1425         # look for "too large" files according to size limit and mark them
1426         root = ET.fromstring(fm)
1427         for e in root.findall('entry'):
1428             size = e.get('size')
1429             if size and self.size_limit and int(size) > self.size_limit \
1430                 or skip_service and (e.get('name').startswith('_service:') or e.get('name').startswith('_service_')):
1431                 e.set('skipped', 'true')
1432         return ET.tostring(root)
1433
1434     def update_datastructs(self):
1435         """
1436         Update the internal data structures if the local _files
1437         file has changed (e.g. update_local_filesmeta() has been
1438         called).
1439         """
1440         import fnmatch
1441         files_tree = read_filemeta(self.dir)
1442         files_tree_root = files_tree.getroot()
1443
1444         self.rev = files_tree_root.get('rev')
1445         self.srcmd5 = files_tree_root.get('srcmd5')
1446
1447         self.linkinfo = Linkinfo()
1448         self.linkinfo.read(files_tree_root.find('linkinfo'))
1449
1450         self.filenamelist = []
1451         self.filelist = []
1452         self.skipped = []
1453         for node in files_tree_root.findall('entry'):
1454             try:
1455                 f = File(node.get('name'),
1456                          node.get('md5'),
1457                          int(node.get('size')),
1458                          int(node.get('mtime')))
1459                 if node.get('skipped'):
1460                     self.skipped.append(f.name)
1461                     f.skipped = True
1462             except:
1463                 # okay, a very old version of _files, which didn't contain any metadata yet...
1464                 f = File(node.get('name'), '', 0, 0)
1465             self.filelist.append(f)
1466             self.filenamelist.append(f.name)
1467
1468         self.to_be_added = read_tobeadded(self.absdir)
1469         self.to_be_deleted = read_tobedeleted(self.absdir)
1470         self.in_conflict = read_inconflict(self.absdir)
1471         self.linkrepair = os.path.isfile(os.path.join(self.storedir, '_linkrepair'))
1472         self.size_limit = read_sizelimit(self.dir)
1473         self.meta = self.ismetamode()
1474
1475         # gather unversioned files, but ignore some stuff
1476         self.excluded = []
1477         for i in os.listdir(self.dir):
1478             for j in conf.config['exclude_glob']:
1479                 if fnmatch.fnmatch(i, j):
1480                     self.excluded.append(i)
1481                     break
1482         self.filenamelist_unvers = [ i for i in os.listdir(self.dir)
1483                                      if i not in self.excluded
1484                                      if i not in self.filenamelist ]
1485
1486     def islink(self):
1487         """tells us if the package is a link (has 'linkinfo').
1488         A package with linkinfo is a package which links to another package.
1489         Returns True if the package is a link, otherwise False."""
1490         return self.linkinfo.islink()
1491
1492     def isexpanded(self):
1493         """tells us if the package is a link which is expanded.
1494         Returns True if the package is expanded, otherwise False."""
1495         return self.linkinfo.isexpanded()
1496
1497     def islinkrepair(self):
1498         """tells us if we are repairing a broken source link."""
1499         return self.linkrepair
1500
1501     def ispulled(self):
1502         """tells us if we have pulled a link."""
1503         return os.path.isfile(os.path.join(self.storedir, '_pulled'))
1504
1505     def isfrozen(self):
1506         """tells us if the link is frozen."""
1507         return os.path.isfile(os.path.join(self.storedir, '_frozenlink'))
1508
1509     def ismetamode(self):
1510         """tells us if the package is in meta mode"""
1511         return os.path.isfile(os.path.join(self.storedir, '_meta_mode'))
1512
1513     def get_pulled_srcmd5(self):
1514         pulledrev = None
1515         for line in open(os.path.join(self.storedir, '_pulled'), 'r'):
1516             pulledrev = line.strip()
1517         return pulledrev
1518
1519     def haslinkerror(self):
1520         """
1521         Returns True if the link is broken otherwise False.
1522         If the package is not a link it returns False.
1523         """
1524         return self.linkinfo.haserror()
1525
1526     def linkerror(self):
1527         """
1528         Returns an error message if the link is broken otherwise None.
1529         If the package is not a link it returns None.
1530         """
1531         return self.linkinfo.error
1532
1533     def update_local_pacmeta(self):
1534         """
1535         Update the local _meta file in the store.
1536         It is replaced with the version pulled from upstream.
1537         """
1538         meta = ''.join(show_package_meta(self.apiurl, self.prjname, self.name))
1539         store_write_string(self.absdir, '_meta', meta + '\n')
1540
1541     def findfilebyname(self, n):
1542         for i in self.filelist:
1543             if i.name == n:
1544                 return i
1545
1546     def get_status(self, excluded=False, *exclude_states):
1547         global store
1548         todo = self.todo
1549         if not todo:
1550             todo = self.filenamelist + self.to_be_added + \
1551                 [i for i in self.filenamelist_unvers if not os.path.isdir(os.path.join(self.absdir, i))]
1552             if excluded:
1553                 todo.extend([i for i in self.excluded if i != store])
1554             todo = set(todo)
1555         res = []
1556         for fname in sorted(todo):
1557             st = self.status(fname)
1558             if not st in exclude_states:
1559                 res.append((st, fname))
1560         return res
1561
1562     def status(self, n):
1563         """
1564         status can be:
1565
1566          file  storefile  file present  STATUS
1567         exists  exists      in _files
1568
1569           x       -            -        'A' and listed in _to_be_added
1570           x       x            -        'R' and listed in _to_be_added
1571           x       x            x        ' ' if digest differs: 'M'
1572                                             and if in conflicts file: 'C'
1573           x       -            -        '?'
1574           -       x            x        'D' and listed in _to_be_deleted
1575           x       x            x        'D' and listed in _to_be_deleted (e.g. if deleted file was modified)
1576           x       x            x        'C' and listed in _in_conflict
1577           x       -            x        'S' and listed in self.skipped
1578           -       -            x        'S' and listed in self.skipped
1579           -       x            x        '!'
1580           -       -            -        NOT DEFINED
1581
1582         """
1583
1584         known_by_meta = False
1585         exists = False
1586         exists_in_store = False
1587         if n in self.filenamelist:
1588             known_by_meta = True
1589         if os.path.exists(os.path.join(self.absdir, n)):
1590             exists = True
1591         if os.path.exists(os.path.join(self.storedir, n)):
1592             exists_in_store = True
1593
1594         if n in self.to_be_deleted:
1595             state = 'D'
1596         elif n in self.in_conflict:
1597             state = 'C'
1598         elif n in self.skipped:
1599             state = 'S'
1600         elif n in self.to_be_added and exists and exists_in_store:
1601             state = 'R'
1602         elif n in self.to_be_added and exists:
1603             state = 'A'
1604         elif exists and exists_in_store and known_by_meta:
1605             if dgst(os.path.join(self.absdir, n)) != self.findfilebyname(n).md5:
1606                 state = 'M'
1607             else:
1608                 state = ' '
1609         elif n in self.to_be_added and not exists:
1610             state = '!'
1611         elif not exists and exists_in_store and known_by_meta and not n in self.to_be_deleted:
1612             state = '!'
1613         elif exists and not exists_in_store and not known_by_meta:
1614             state = '?'
1615         elif not exists_in_store and known_by_meta:
1616             # XXX: this codepath shouldn't be reached (we restore the storefile
1617             #      in update_datastructs)
1618             raise oscerr.PackageInternalError(self.prjname, self.name,
1619                 'error: file \'%s\' is known by meta but no storefile exists.\n'
1620                 'This might be caused by an old wc format. Please backup your current\n'
1621                 'wc and checkout the package again. Afterwards copy all files (except the\n'
1622                 '.osc/ dir) into the new package wc.' % n)
1623         else:
1624             # this case shouldn't happen (except there was a typo in the filename etc.)
1625             raise oscerr.OscIOError(None, 'osc: \'%s\' is not under version control' % n)
1626
1627         return state
1628
1629     def get_diff(self, revision=None, ignoreUnversioned=False):
1630         import tempfile
1631         diff_hdr = 'Index: %s\n'
1632         diff_hdr += '===================================================================\n'
1633         kept = []
1634         added = []
1635         deleted = []
1636         def diff_add_delete(fname, add, revision):
1637             diff = []
1638             diff.append(diff_hdr % fname)
1639             tmpfile = None
1640             origname = fname
1641             if add:
1642                 diff.append('--- %s\t(revision 0)\n' % fname)
1643                 rev = 'revision 0'
1644                 if revision and not fname in self.to_be_added:
1645                     rev = 'working copy'
1646                 diff.append('+++ %s\t(%s)\n' % (fname, rev))
1647                 fname = os.path.join(self.absdir, fname)
1648             else:
1649                 diff.append('--- %s\t(revision %s)\n' % (fname, revision or self.rev))
1650                 diff.append('+++ %s\t(working copy)\n' % fname)
1651                 fname = os.path.join(self.storedir, fname)
1652                
1653             try:
1654                 if revision is not None and not add:
1655                     (fd, tmpfile) = tempfile.mkstemp(prefix='osc_diff')
1656                     get_source_file(self.apiurl, self.prjname, self.name, origname, tmpfile, revision)
1657                     fname = tmpfile
1658                 if binary_file(fname):
1659                     what = 'added'
1660                     if not add:
1661                         what = 'deleted'
1662                     diff = diff[:1]
1663                     diff.append('Binary file \'%s\' %s.\n' % (origname, what))
1664                     return diff
1665                 tmpl = '+%s'
1666                 ltmpl = '@@ -0,0 +1,%d @@\n'
1667                 if not add:
1668                     tmpl = '-%s'
1669                     ltmpl = '@@ -1,%d +0,0 @@\n'
1670                 lines = [tmpl % i for i in open(fname, 'r').readlines()]
1671                 if len(lines):
1672                     diff.append(ltmpl % len(lines))
1673                     if not lines[-1].endswith('\n'):
1674                         lines.append('\n\\ No newline at end of file\n')
1675                 diff.extend(lines)
1676             finally:
1677                 if tmpfile is not None:
1678                     os.close(fd)
1679                     os.unlink(tmpfile)
1680             return diff
1681
1682         if revision is None:
1683             todo = self.todo or [i for i in self.filenamelist if not i in self.to_be_added]+self.to_be_added
1684             for fname in todo:
1685                 if fname in self.to_be_added and self.status(fname) == 'A':
1686                     added.append(fname)
1687                 elif fname in self.to_be_deleted:
1688                     deleted.append(fname)
1689                 elif fname in self.filenamelist:
1690                     kept.append(self.findfilebyname(fname))
1691                 elif fname in self.to_be_added and self.status(fname) == '!':
1692                     raise oscerr.OscIOError(None, 'file \'%s\' is marked as \'A\' but does not exist\n'\
1693                         '(either add the missing file or revert it)' % fname)
1694                 elif not ignoreUnversioned:
1695                     raise oscerr.OscIOError(None, 'file \'%s\' is not under version control' % fname)
1696         else:
1697             fm = self.get_files_meta(revision=revision)
1698             root = ET.fromstring(fm)
1699             rfiles = self.__get_files(root)
1700             # swap added and deleted
1701             kept, deleted, added, services = self.__get_rev_changes(rfiles)
1702             added = [f.name for f in added]
1703             added.extend([f for f in self.to_be_added if not f in kept])
1704             deleted = [f.name for f in deleted]
1705             deleted.extend(self.to_be_deleted)
1706             for f in added[:]:
1707                 if f in deleted:
1708                     added.remove(f)
1709                     deleted.remove(f)
1710 #        print kept, added, deleted
1711         for f in kept:
1712             state = self.status(f.name)
1713             if state in ('S', '?', '!'):
1714                 continue
1715             elif state == ' ' and revision is None:
1716                 continue
1717             elif revision and self.findfilebyname(f.name).md5 == f.md5 and state != 'M':
1718                 continue
1719             yield [diff_hdr % f.name]
1720             if revision is None:
1721                 yield get_source_file_diff(self.absdir, f.name, self.rev)
1722             else:
1723                 tmpfile = None
1724                 diff = []
1725                 try:
1726                     (fd, tmpfile) = tempfile.mkstemp(prefix='osc_diff')
1727                     get_source_file(self.apiurl, self.prjname, self.name, f.name, tmpfile, revision)
1728                     diff = get_source_file_diff(self.absdir, f.name, revision,
1729                         os.path.basename(tmpfile), os.path.dirname(tmpfile), f.name)
1730                 finally:
1731                     if tmpfile is not None:
1732                         os.close(fd)
1733                         os.unlink(tmpfile)
1734                 yield diff
1735
1736         for f in added:
1737             yield diff_add_delete(f, True, revision)
1738         for f in deleted:
1739             yield diff_add_delete(f, False, revision)
1740
1741     def merge(self, otherpac):
1742         self.todo += otherpac.todo
1743
1744     def __str__(self):
1745         r = """
1746 name: %s
1747 prjname: %s
1748 workingdir: %s
1749 localfilelist: %s
1750 linkinfo: %s
1751 rev: %s
1752 'todo' files: %s
1753 """ % (self.name,
1754         self.prjname,
1755         self.dir,
1756         '\n               '.join(self.filenamelist),
1757         self.linkinfo,
1758         self.rev,
1759         self.todo)
1760
1761         return r
1762
1763
1764     def read_meta_from_spec(self, spec = None):
1765         import glob
1766         if spec:
1767             specfile = spec
1768         else:
1769             # scan for spec files
1770             speclist = glob.glob(os.path.join(self.dir, '*.spec'))
1771             if len(speclist) == 1:
1772                 specfile = speclist[0]
1773             elif len(speclist) > 1:
1774                 print 'the following specfiles were found:'
1775                 for filename in speclist:
1776                     print filename
1777                 print 'please specify one with --specfile'
1778                 sys.exit(1)
1779             else:
1780                 print 'no specfile was found - please specify one ' \
1781                       'with --specfile'
1782                 sys.exit(1)
1783
1784         data = read_meta_from_spec(specfile, 'Summary', 'Url', '%description')
1785         self.summary = data.get('Summary', '')
1786         self.url = data.get('Url', '')
1787         self.descr = data.get('%description', '')
1788
1789
1790     def update_package_meta(self, force=False):
1791         """
1792         for the updatepacmetafromspec subcommand
1793             argument force supress the confirm question
1794         """
1795
1796         m = ''.join(show_package_meta(self.apiurl, self.prjname, self.name))
1797
1798         root = ET.fromstring(m)
1799         root.find('title').text = self.summary
1800         root.find('description').text = ''.join(self.descr)
1801         url = root.find('url')
1802         if url == None:
1803             url = ET.SubElement(root, 'url')
1804         url.text = self.url
1805
1806         u = makeurl(self.apiurl, ['source', self.prjname, self.name, '_meta'])
1807         mf = metafile(u, ET.tostring(root))
1808
1809         if not force:
1810             print '*' * 36, 'old', '*' * 36
1811             print m
1812             print '*' * 36, 'new', '*' * 36
1813             print ET.tostring(root)
1814             print '*' * 72
1815             repl = raw_input('Write? (y/N/e) ')
1816         else:
1817             repl = 'y'
1818
1819         if repl == 'y':
1820             mf.sync()
1821         elif repl == 'e':
1822             mf.edit()
1823
1824         mf.discard()
1825
1826     def mark_frozen(self):
1827         store_write_string(self.absdir, '_frozenlink', '')
1828         print
1829         print "The link in this package is currently broken. Checking"
1830         print "out the last working version instead; please use 'osc pull'"
1831         print "to repair the link."
1832         print
1833
1834     def unmark_frozen(self):
1835         if os.path.exists(os.path.join(self.storedir, '_frozenlink')):
1836             os.unlink(os.path.join(self.storedir, '_frozenlink'))
1837
1838     def latest_rev(self, include_service_files=False):
1839         if self.islinkrepair():
1840             upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrepair=1, meta=self.meta, include_service_files=include_service_files)
1841         elif self.islink() and self.isexpanded():
1842             if self.isfrozen() or self.ispulled():
1843                 upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev=self.linkinfo.srcmd5, meta=self.meta, include_service_files=include_service_files)
1844             else:
1845                 try:
1846                     upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, meta=self.meta, include_service_files=include_service_files)
1847                 except:
1848                     try:
1849                         upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev=self.linkinfo.srcmd5, meta=self.meta, include_service_files=include_service_files)
1850                     except:
1851                         upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev="base", meta=self.meta, include_service_files=include_service_files)
1852                     self.mark_frozen()
1853         else:
1854             upstream_rev = show_upstream_rev(self.apiurl, self.prjname, self.name, meta=self.meta, include_service_files=include_service_files)
1855         return upstream_rev
1856
1857     def __get_files(self, fmeta_root):
1858         f = []
1859         if fmeta_root.get('rev') is None and len(fmeta_root.findall('entry')) > 0:
1860             raise oscerr.APIError('missing rev attribute in _files:\n%s' % ''.join(ET.tostring(fmeta_root)))
1861         for i in fmeta_root.findall('entry'):
1862             skipped = i.get('skipped') is not None
1863             f.append(File(i.get('name'), i.get('md5'),
1864                      int(i.get('size')), int(i.get('mtime')), skipped))
1865         return f
1866
1867     def __get_rev_changes(self, revfiles):
1868         kept = []
1869         added = []
1870         deleted = []
1871         services = []
1872         revfilenames = []
1873         for f in revfiles:
1874             revfilenames.append(f.name)
1875             # treat skipped like deleted files
1876             if f.skipped:
1877                 if f.name.startswith('_service:'):
1878                     services.append(f)
1879                 else:
1880                     deleted.append(f)
1881                 continue
1882             # treat skipped like added files
1883             # problem: this overwrites existing files during the update
1884             # (because skipped files aren't in self.filenamelist_unvers)
1885             if f.name in self.filenamelist and not f.name in self.skipped:
1886                 kept.append(f)
1887             else:
1888                 added.append(f)
1889         for f in self.filelist:
1890             if not f.name in revfilenames:
1891                 deleted.append(f)
1892
1893         return kept, added, deleted, services
1894
1895     def update(self, rev = None, service_files = False, size_limit = None):
1896         import tempfile
1897         rfiles = []
1898         # size_limit is only temporary for this update
1899         old_size_limit = self.size_limit
1900         if not size_limit is None:
1901             self.size_limit = int(size_limit)
1902         if os.path.isfile(os.path.join(self.storedir, '_in_update', '_files')):
1903             print 'resuming broken update...'
1904             root = ET.parse(os.path.join(self.storedir, '_in_update', '_files')).getroot()
1905             rfiles = self.__get_files(root)
1906             kept, added, deleted, services = self.__get_rev_changes(rfiles)
1907             # check if we aborted in the middle of a file update
1908             broken_file = os.listdir(os.path.join(self.storedir, '_in_update'))
1909             broken_file.remove('_files')
1910             if len(broken_file) == 1:
1911                 origfile = os.path.join(self.storedir, '_in_update', broken_file[0])
1912                 wcfile = os.path.join(self.absdir, broken_file[0])
1913                 origfile_md5 = dgst(origfile)
1914                 origfile_meta = self.findfilebyname(broken_file[0])
1915                 if origfile.endswith('.copy'):
1916                     # ok it seems we aborted at some point during the copy process
1917                     # (copy process == copy wcfile to the _in_update dir). remove file+continue
1918                     os.unlink(origfile)
1919                 elif self.findfilebyname(broken_file[0]) is None:
1920                     # should we remove this file from _in_update? if we don't
1921                     # the user has no chance to continue without removing the file manually
1922                     raise oscerr.PackageInternalError(self.prjname, self.name,
1923                         '\'%s\' is not known by meta but exists in \'_in_update\' dir')
1924                 elif os.path.isfile(wcfile) and dgst(wcfile) != origfile_md5:
1925                     (fd, tmpfile) = tempfile.mkstemp(dir=self.absdir, prefix=broken_file[0]+'.')
1926                     os.close(fd)
1927                     os.rename(wcfile, tmpfile)
1928                     os.rename(origfile, wcfile)
1929                     print 'warning: it seems you modified \'%s\' after the broken ' \
1930                           'update. Restored original file and saved modified version ' \
1931                           'to \'%s\'.' % (wcfile, tmpfile)
1932                 elif not os.path.isfile(wcfile):
1933                     # this is strange... because it existed before the update. restore it
1934                     os.rename(origfile, wcfile)
1935                 else:
1936                     # everything seems to be ok
1937                     os.unlink(origfile)
1938             elif len(broken_file) > 1:
1939                 raise oscerr.PackageInternalError(self.prjname, self.name, 'too many files in \'_in_update\' dir')
1940             tmp = rfiles[:]
1941             for f in tmp:
1942                 if os.path.exists(os.path.join(self.storedir, f.name)):
1943                     if dgst(os.path.join(self.storedir, f.name)) == f.md5:
1944                         if f in kept:
1945                             kept.remove(f)
1946                         elif f in added:
1947                             added.remove(f)
1948                         # this can't happen
1949                         elif f in deleted:
1950                             deleted.remove(f)
1951             if not service_files:
1952                 services = []
1953             self.__update(kept, added, deleted, services, ET.tostring(root), root.get('rev'))
1954             os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
1955             os.rmdir(os.path.join(self.storedir, '_in_update'))
1956         # ok everything is ok (hopefully)...
1957         fm = self.get_files_meta(revision=rev)
1958         root = ET.fromstring(fm)
1959         rfiles = self.__get_files(root)
1960         store_write_string(self.absdir, '_files', fm + '\n', subdir='_in_update')
1961         kept, added, deleted, services = self.__get_rev_changes(rfiles)
1962         if not service_files:
1963             services = []
1964         self.__update(kept, added, deleted, services, fm, root.get('rev'))
1965         os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
1966         if os.path.isdir(os.path.join(self.storedir, '_in_update')):
1967             os.rmdir(os.path.join(self.storedir, '_in_update'))
1968         self.size_limit = old_size_limit
1969
1970     def __update(self, kept, added, deleted, services, fm, rev):
1971         pathn = getTransActPath(self.dir)
1972         # check for conflicts with existing files
1973         for f in added:
1974             if f.name in self.filenamelist_unvers:
1975                 raise oscerr.PackageFileConflict(self.prjname, self.name, f.name,
1976                     'failed to add file \'%s\' file/dir with the same name already exists' % f.name)
1977         # ok, the update can't fail due to existing files
1978         for f in added:
1979             self.updatefile(f.name, rev, f.mtime)
1980             print statfrmt('A', os.path.join(pathn, f.name))
1981         for f in deleted:
1982             # if the storefile doesn't exist we're resuming an aborted update:
1983             # the file was already deleted but we cannot know this
1984             # OR we're processing a _service: file (simply keep the file)
1985             if os.path.isfile(os.path.join(self.storedir, f.name)) and self.status(f.name) != 'M':
1986 #            if self.status(f.name) != 'M':
1987                 self.delete_localfile(f.name)
1988             self.delete_storefile(f.name)
1989             print statfrmt('D', os.path.join(pathn, f.name))
1990             if f.name in self.to_be_deleted:
1991                 self.to_be_deleted.remove(f.name)
1992                 self.write_deletelist()
1993
1994         for f in kept:
1995             state = self.status(f.name)
1996 #            print f.name, state
1997             if state == 'M' and self.findfilebyname(f.name).md5 == f.md5:
1998                 # remote file didn't change
1999                 pass
2000             elif state == 'M':
2001                 # try to merge changes
2002                 merge_status = self.mergefile(f.name, rev, f.mtime)
2003                 print statfrmt(merge_status, os.path.join(pathn, f.name))
2004             elif state == '!':
2005                 self.updatefile(f.name, rev, f.mtime)
2006                 print 'Restored \'%s\'' % os.path.join(pathn, f.name)
2007             elif state == 'C':
2008                 get_source_file(self.apiurl, self.prjname, self.name, f.name,
2009                     targetfilename=os.path.join(self.storedir, f.name), revision=rev,
2010                     progress_obj=self.progress_obj, mtime=f.mtime, meta=self.meta)
2011                 print 'skipping \'%s\' (this is due to conflicts)' % f.name
2012             elif state == 'D' and self.findfilebyname(f.name).md5 != f.md5:
2013                 # XXX: in the worst case we might end up with f.name being
2014                 # in _to_be_deleted and in _in_conflict... this needs to be checked
2015                 if os.path.exists(os.path.join(self.absdir, f.name)):
2016                     merge_status = self.mergefile(f.name, rev, f.mtime)
2017                     print statfrmt(merge_status, os.path.join(pathn, f.name))
2018                     if merge_status == 'C':
2019                         # state changes from delete to conflict
2020                         self.to_be_deleted.remove(f.name)
2021                         self.write_deletelist()
2022                 else:
2023                     # XXX: we cannot recover this case because we've no file
2024                     # to backup
2025                     self.updatefile(f.name, rev, f.mtime)
2026                     print statfrmt('U', os.path.join(pathn, f.name))
2027             elif state == ' ' and self.findfilebyname(f.name).md5 != f.md5:
2028                 self.updatefile(f.name, rev, f.mtime)
2029                 print statfrmt('U', os.path.join(pathn, f.name))
2030
2031         # checkout service files
2032         for f in services:
2033             get_source_file(self.apiurl, self.prjname, self.name, f.name,
2034                 targetfilename=os.path.join(self.absdir, f.name), revision=rev,
2035                 progress_obj=self.progress_obj, mtime=f.mtime, meta=self.meta)
2036             print statfrmt('A', os.path.join(pathn, f.name))
2037         store_write_string(self.absdir, '_files', fm + '\n')
2038         if not self.meta:
2039             self.update_local_pacmeta()
2040         self.update_datastructs()
2041
2042         print 'At revision %s.' % self.rev
2043
2044     def run_source_services(self, mode=None, singleservice=None):
2045         curdir = os.getcwd()
2046         os.chdir(self.absdir) # e.g. /usr/lib/obs/service/verify_file fails if not inside the project dir.
2047         si = Serviceinfo()
2048         if self.filenamelist.count('_service') or self.filenamelist_unvers.count('_service'):
2049             service = ET.parse(os.path.join(self.absdir, '_service')).getroot()
2050             si.read(service)
2051         si.getProjectGlobalServices(self.apiurl, self.prjname, self.name)
2052         r = si.execute(self.absdir, mode, singleservice)
2053         os.chdir(curdir)
2054         return r
2055
2056     def prepare_filelist(self):
2057         """Prepare a list of files, which will be processed by process_filelist
2058         method. This allows easy modifications of a file list in commit
2059         phase.
2060         """
2061         if not self.todo:
2062             self.todo = self.filenamelist + self.filenamelist_unvers
2063         self.todo.sort()
2064
2065         ret = ""
2066         for f in [f for f in self.todo if not os.path.isdir(f)]:
2067             action = 'leave'
2068             status = self.status(f)
2069             if status == 'S':
2070                 continue
2071             if status == '!':
2072                 action = 'remove'
2073             ret += "%s %s %s\n" % (action, status, f)
2074
2075         ret += """
2076 # Edit a filelist for package \'%s\'
2077 # Commands:
2078 # l, leave = leave a file as is
2079 # r, remove = remove a file
2080 # a, add   = add a file
2081 #
2082 # If you remove file from a list, it will be unchanged
2083 # If you remove all, commit will be aborted""" % self.name
2084
2085         return ret
2086
2087     def edit_filelist(self):
2088         """Opens a package list in editor for editing. This allows easy
2089         modifications of it just by simple text editing
2090         """
2091
2092         import tempfile
2093         (fd, filename) = tempfile.mkstemp(prefix = 'osc-filelist', suffix = '.txt')
2094         f = os.fdopen(fd, 'w')
2095         f.write(self.prepare_filelist())
2096         f.close()
2097         mtime_orig = os.stat(filename).st_mtime
2098
2099         while 1:
2100             run_editor(filename)
2101             mtime = os.stat(filename).st_mtime
2102             if mtime_orig < mtime:
2103                 filelist = open(filename).readlines()
2104                 os.unlink(filename)
2105                 break
2106             else:
2107                 raise oscerr.UserAbort()
2108
2109         return self.process_filelist(filelist)
2110
2111     def process_filelist(self, filelist):
2112         """Process a filelist - it add/remove or leave files. This depends on
2113         user input. If no file is processed, it raises an ValueError
2114         """
2115
2116         loop = False
2117         for line in [l.strip() for l in filelist if (l[0] != "#" or l.strip() != '')]:
2118
2119             foo = line.split(' ')
2120             if len(foo) == 4:
2121                 action, state, name = (foo[0], ' ', foo[3])
2122             elif len(foo) == 3:
2123                 action, state, name = (foo[0], foo[1], foo[2])
2124             else:
2125                 break
2126             action = action.lower()
2127             loop = True
2128
2129             if action in ('r', 'remove'):
2130                 if self.status(name) == '?':
2131                     os.unlink(name)
2132                     if name in self.todo:
2133                         self.todo.remove(name)
2134                 else:
2135                     self.delete_file(name, True)
2136             elif action in ('a', 'add'):
2137                 if self.status(name) != '?':
2138                     print "Cannot add file %s with state %s, skipped" % (name, self.status(name))
2139                 else:
2140                     self.addfile(name)
2141             elif action in ('l', 'leave'):
2142                 pass
2143             else:
2144                 raise ValueError("Unknow action `%s'" % action)
2145
2146         if not loop:
2147             raise ValueError("Empty filelist")
2148
2149     def revert(self, filename):
2150         if not filename in self.filenamelist and not filename in self.to_be_added:
2151             raise oscerr.OscIOError(None, 'file \'%s\' is not under version control' % filename)
2152         elif filename in self.skipped:
2153             raise oscerr.OscIOError(None, 'file \'%s\' is marked as skipped and cannot be reverted' % filename)
2154         if filename in self.filenamelist and not os.path.exists(os.path.join(self.storedir, filename)):
2155             raise oscerr.PackageInternalError('file \'%s\' is listed in filenamelist but no storefile exists' % filename)
2156         state = self.status(filename)
2157         if not (state == 'A' or state == '!' and filename in self.to_be_added):
2158             shutil.copyfile(os.path.join(self.storedir, filename), os.path.join(self.absdir, filename))
2159         if state == 'D':
2160             self.to_be_deleted.remove(filename)
2161             self.write_deletelist()
2162         elif state == 'C':
2163             self.clear_from_conflictlist(filename)
2164         elif state in ('A', 'R') or state == '!' and filename in self.to_be_added:
2165             self.to_be_added.remove(filename)
2166             self.write_addlist()
2167
2168     @staticmethod
2169     def init_package(apiurl, project, package, dir, size_limit=None, meta=False, progress_obj=None):
2170         global store
2171
2172         if not os.path.exists(dir):
2173             os.mkdir(dir)
2174         elif not os.path.isdir(dir):
2175             raise oscerr.OscIOError(None, 'error: \'%s\' is no directory' % dir)
2176         if os.path.exists(os.path.join(dir, store)):
2177             raise oscerr.OscIOError(None, 'error: \'%s\' is already an initialized osc working copy' % dir)
2178         else:
2179             os.mkdir(os.path.join(dir, store))
2180         store_write_project(dir, project)
2181         store_write_string(dir, '_package', package + '\n')
2182         store_write_apiurl(dir, apiurl)
2183         if meta:
2184             store_write_string(dir, '_meta_mode', '')
2185         if size_limit:
2186             store_write_string(dir, '_size_limit', str(size_limit) + '\n')
2187         store_write_string(dir, '_files', '<directory />' + '\n')
2188         store_write_string(dir, '_osclib_version', __store_version__ + '\n')
2189         return Package(dir, progress_obj=progress_obj, size_limit=size_limit)
2190
2191
2192 class AbstractState:
2193     """
2194     Base class which represents state-like objects (<review />, <state />).
2195     """
2196     def __init__(self, tag):
2197         self.__tag = tag
2198
2199     def get_node_attrs(self):
2200         """return attributes for the tag/element"""
2201         raise NotImplementedError()
2202
2203     def get_node_name(self):
2204         """return tag/element name"""
2205         return self.__tag
2206
2207     def get_comment(self):
2208         """return data from <comment /> tag"""
2209         raise NotImplementedError()
2210
2211     def to_xml(self):
2212         """serialize object to XML"""
2213         root = ET.Element(self.get_node_name())
2214         for attr in self.get_node_attrs():
2215             val = getattr(self, attr)
2216             if not val is None:
2217                 root.set(attr, val)
2218         if self.get_comment():
2219             ET.SubElement(root, 'comment').text = self.get_comment()
2220         return root
2221
2222     def to_str(self):
2223         """return "pretty" XML data"""
2224         root = self.to_xml()
2225         xmlindent(root)
2226         return ET.tostring(root)
2227
2228
2229 class ReviewState(AbstractState):
2230     """Represents the review state in a request"""
2231     def __init__(self, review_node):
2232         if not review_node.get('state'):
2233             raise oscerr.APIError('invalid review node (state attr expected): %s' % \
2234                 ET.tostring(review_node))
2235         AbstractState.__init__(self, review_node.tag)
2236         self.state = review_node.get('state')
2237         self.by_user = review_node.get('by_user')
2238         self.by_group = review_node.get('by_group')
2239         self.by_project = review_node.get('by_project')
2240         self.by_package = review_node.get('by_package')
2241         self.who = review_node.get('who')
2242         self.when = review_node.get('when')
2243         self.comment = ''
2244         if not review_node.find('comment') is None and \
2245             review_node.find('comment').text:
2246             self.comment = review_node.find('comment').text.strip()
2247
2248     def get_node_attrs(self):
2249         return ('state', 'by_user', 'by_group', 'by_project', 'by_package', 'who', 'when')
2250
2251     def get_comment(self):
2252         return self.comment
2253
2254
2255 class RequestState(AbstractState):
2256     """Represents the state of a request"""
2257     def __init__(self, state_node):
2258         if not state_node.get('name'):
2259             raise oscerr.APIError('invalid request state node (name attr expected): %s' % \
2260                 ET.tostring(state_node))
2261         AbstractState.__init__(self, state_node.tag)
2262         self.name = state_node.get('name')
2263         self.who = state_node.get('who')
2264         self.when = state_node.get('when')
2265         self.comment = ''
2266         if not state_node.find('comment') is None and \
2267             state_node.find('comment').text:
2268             self.comment = state_node.find('comment').text.strip()
2269
2270     def get_node_attrs(self):
2271         return ('name', 'who', 'when')
2272
2273     def get_comment(self):
2274         return self.comment
2275
2276
2277 class Action:
2278     """
2279     Represents a <action /> element of a Request.
2280     This class is quite common so that it can be used for all different
2281     action types. Note: instances only provide attributes for their specific
2282     type.
2283     Examples:
2284       r = Action('set_bugowner', tgt_project='foo', person_name='buguser')
2285       # available attributes: r.type (== 'set_bugowner'), r.tgt_project (== 'foo'), r.tgt_package (== None)
2286       r.to_str() ->
2287       <action type="set_bugowner">
2288         <target project="foo" />
2289         <person name="buguser" />
2290       </action>
2291       ##
2292       r = Action('delete', tgt_project='foo', tgt_package='bar')
2293       # available attributes: r.type (== 'delete'), r.tgt_project (== 'foo'), r.tgt_package (=='bar')
2294       r.to_str() ->
2295       <action type="delete">
2296         <target package="bar" project="foo" />
2297       </action>
2298     """
2299
2300     # allowed types + the corresponding (allowed) attributes
2301     type_args = {'submit': ('src_project', 'src_package', 'src_rev', 'tgt_project', 'tgt_package', 'opt_sourceupdate',
2302                             'acceptinfo_rev', 'acceptinfo_srcmd5', 'acceptinfo_xsrcmd5', 'acceptinfo_osrcmd5',
2303                             'acceptinfo_oxsrcmd5', 'opt_updatelink'),
2304         'add_role': ('tgt_project', 'tgt_package', 'person_name', 'person_role', 'group_name', 'group_role'),
2305         'set_bugowner': ('tgt_project', 'tgt_package', 'person_name'), # obsoleted by add_role
2306         'maintenance_release': ('src_project', 'src_package', 'src_rev', 'tgt_project', 'tgt_package', 'person_name'),
2307         'maintenance_incident': ('src_project', 'tgt_project', 'person_name'),
2308         'delete': ('tgt_project', 'tgt_package'),
2309         'change_devel': ('src_project', 'src_package', 'tgt_project', 'tgt_package')}
2310     # attribute prefix to element name map (only needed for abbreviated attributes)
2311     prefix_to_elm = {'src': 'source', 'tgt': 'target', 'opt': 'options'}
2312
2313     def __init__(self, type, **kwargs):
2314         if not type in Action.type_args.keys():
2315             raise oscerr.WrongArgs('invalid action type: \'%s\'' % type)
2316         self.type = type
2317         for i in kwargs.keys():
2318             if not i in Action.type_args[type]:
2319                 raise oscerr.WrongArgs('invalid argument: \'%s\'' % i)
2320         # set all type specific attributes
2321         for i in Action.type_args[type]:
2322             if kwargs.has_key(i):
2323                 setattr(self, i, kwargs[i])
2324             else:
2325                 setattr(self, i, None)
2326
2327     def to_xml(self):
2328         """
2329         Serialize object to XML.
2330         The xml tag names and attributes are constructed from the instance's attributes.
2331         Example:
2332           self.group_name  -> tag name is "group", attribute name is "name"
2333           self.src_project -> tag name is "source" (translated via prefix_to_elm dict),
2334                               attribute name is "project"
2335         Attributes prefixed with "opt_" need a special handling, the resulting xml should
2336         look like this: opt_updatelink -> <options><updatelink>value</updatelink></options>.
2337         Attributes which are "None" will be skipped.
2338         """
2339         root = ET.Element('action', type=self.type)
2340         for i in Action.type_args[self.type]:
2341             prefix, attr = i.split('_', 1)
2342             val = getattr(self, i)
2343             if val is None:
2344                 continue
2345             elm = root.find(Action.prefix_to_elm.get(prefix, prefix))
2346             if elm is None:
2347                 elm = ET.Element(Action.prefix_to_elm.get(prefix, prefix))
2348                 root.append(elm)
2349             if prefix == 'opt':
2350                 ET.SubElement(elm, attr).text = val
2351             else:
2352                 elm.set(attr, val)
2353         return root
2354
2355     def to_str(self):
2356         """return "pretty" XML data"""
2357         root = self.to_xml()
2358         xmlindent(root)
2359         return ET.tostring(root)
2360
2361     @staticmethod
2362     def from_xml(action_node):
2363         """create action from XML"""
2364         if action_node is None or \
2365             not action_node.get('type') in Action.type_args.keys() or \
2366             not action_node.tag in ('action', 'submit'):
2367             raise oscerr.WrongArgs('invalid argument')
2368         elm_to_prefix = dict([(i[1], i[0]) for i in Action.prefix_to_elm.items()])
2369         kwargs = {}
2370         for node in action_node:
2371             prefix = elm_to_prefix.get(node.tag, node.tag)
2372             if prefix == 'opt':
2373                 data = [('opt_%s' % opt.tag, opt.text.strip()) for opt in node if opt.text]
2374             else:
2375                 data = [('%s_%s' % (prefix, k), v) for k, v in node.items()]
2376             kwargs.update(dict(data))
2377         return Action(action_node.get('type'), **kwargs)
2378
2379
2380 class Request:
2381     """Represents a request (<request />)"""
2382
2383     def __init__(self):
2384         self._init_attributes()
2385
2386     def _init_attributes(self):
2387         """initialize attributes with default values"""
2388         self.reqid = None
2389         self.title = ''
2390         self.description = ''
2391         self.state = None
2392         self.actions = []
2393         self.statehistory = []
2394         self.reviews = []
2395
2396     def read(self, root):
2397         """read in a request"""
2398         self._init_attributes()
2399         if not root.get('id'):
2400             raise oscerr.APIError('invalid request: %s\n' % ET.tostring(root))
2401         self.reqid = root.get('id')
2402         if root.find('state') is None:
2403             raise oscerr.APIError('invalid request (state expected): %s\n' % ET.tostring(root))
2404         self.state = RequestState(root.find('state'))
2405         action_nodes = root.findall('action')
2406         if not action_nodes:
2407             # check for old-style requests
2408             for i in root.findall('submit'):
2409                 i.set('type', 'submit')
2410                 action_nodes.append(i)
2411         for action in action_nodes:
2412             self.actions.append(Action.from_xml(action))
2413         for review in root.findall('review'):
2414             self.reviews.append(ReviewState(review))
2415         for hist_state in root.findall('history'):
2416             self.statehistory.append(RequestState(hist_state))
2417         if not root.find('title') is None:
2418             self.title = root.find('title').text.strip()
2419         if not root.find('description') is None and root.find('description').text:
2420             self.description = root.find('description').text.strip()
2421
2422     def add_action(self, type, **kwargs):
2423         """add a new action to the request"""
2424         self.actions.append(Action(type, **kwargs))
2425
2426     def get_actions(self, *types):
2427         """
2428         get all actions with a specific type
2429         (if types is empty return all actions)
2430         """
2431         if not types:
2432             return self.actions
2433         return [i for i in self.actions if i.type in types]
2434
2435     def get_creator(self):
2436         """return the creator of the request"""
2437         if len(self.statehistory):
2438             return self.statehistory[0].who
2439         return self.state.who
2440
2441     def to_xml(self):
2442         """serialize object to XML"""
2443         root = ET.Element('request')
2444         if not self.reqid is None:
2445             root.set('id', self.reqid)
2446         for action in self.actions:
2447             root.append(action.to_xml())
2448         if not self.state is None:
2449             root.append(self.state.to_xml())
2450         for review in self.reviews:
2451             root.append(review.to_xml())
2452         for hist in self.statehistory:
2453             root.append(hist.to_xml())
2454         if self.title:
2455             ET.SubElement(root, 'title').text = self.title
2456         if self.description:
2457             ET.SubElement(root, 'description').text = self.description
2458         return root
2459
2460     def to_str(self):
2461         """return "pretty" XML data"""
2462         root = self.to_xml()
2463         xmlindent(root)
2464         return ET.tostring(root)
2465
2466     @staticmethod
2467     def format_review(review, show_srcupdate=False):
2468         """
2469         format a review depending on the reviewer's type.
2470         A dict which contains the formatted str's is returned.
2471         """
2472
2473         d = {'state': '%s:' % review.state}
2474         if review.by_package:
2475            d['by'] = '%s/%s' % (review.by_project, review.by_package)
2476            d['type'] = 'Package'
2477         elif review.by_project:
2478            d['by'] = '%s' % review.by_project
2479            d['type'] = 'Project'
2480         elif review.by_group:
2481            d['by'] = '%s' % review.by_group
2482            d['type'] = 'Group'
2483         else:
2484            d['by'] = '%s' % review.by_user
2485            d['type'] = 'User'
2486         if review.who:
2487            d['by'] += '(%s)' % review.who
2488         return d
2489
2490     @staticmethod
2491     def format_action(action, show_srcupdate=False):
2492         """
2493         format an action depending on the action's type.
2494         A dict which contains the formatted str's is returned.
2495         """
2496         def prj_pkg_join(prj, pkg):
2497             if not pkg:
2498                 return prj or ''
2499             return '%s/%s' % (prj, pkg)
2500
2501         d = {'type': '%s:' % action.type}
2502         if action.type == 'set_bugowner':
2503             d['source'] = action.person_name
2504             d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2505         elif action.type == 'change_devel':
2506             d['source'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2507             d['target'] = 'developed in %s' % prj_pkg_join(action.src_project, action.src_package)
2508         elif action.type == 'maintenance_incident':
2509             d['source'] = '%s ->' % action.src_project
2510             d['target'] = action.tgt_project
2511         elif action.type == 'maintenance_release':
2512             d['source'] = '%s ->' % prj_pkg_join(action.src_project, action.src_package)
2513             d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2514         elif action.type == 'submit':
2515             srcupdate = ' '
2516             if action.opt_sourceupdate and show_srcupdate:
2517                 srcupdate = '(%s)' % action.opt_sourceupdate
2518             d['source'] = '%s%s ->' % (prj_pkg_join(action.src_project, action.src_package), srcupdate)
2519             tgt_package = action.tgt_package
2520             if action.src_package == action.tgt_package:
2521                 tgt_package = ''
2522             d['target'] = prj_pkg_join(action.tgt_project, tgt_package)
2523         elif action.type == 'add_role':
2524             roles = []
2525             if action.person_name and action.person_role:
2526                 roles.append('person: %s as %s' % (action.person_name, action.person_role))
2527             if action.group_name and action.group_role:
2528                 roles.append('group: %s as %s' % (action.group_name, action.group_role))
2529             d['source'] = ', '.join(roles)
2530             d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2531         elif action.type == 'delete':
2532             d['source'] = ''
2533             d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2534         return d
2535
2536     def list_view(self):
2537         """return "list view" format"""
2538         import textwrap
2539         lines = ['%6s  State:%-10s By:%-12s When:%-19s' % (self.reqid, self.state.name, self.state.who, self.state.when)]
2540         tmpl = '        %(type)-16s %(source)-50s %(target)s'
2541         for action in self.actions:
2542             lines.append(tmpl % Request.format_action(action))
2543         tmpl = '        Review by %(type)-10s is %(state)-10s %(by)-50s'
2544         for review in self.reviews:
2545             lines.append(tmpl % Request.format_review(review))
2546         history = ['%s(%s)' % (hist.name, hist.who) for hist in self.statehistory]
2547         if history:
2548             lines.append('        From: %s' % ' -> '.join(history))
2549         if self.description:
2550             lines.append(textwrap.fill(self.description, width=80, initial_indent='        Descr: ',
2551                 subsequent_indent='               '))
2552         return '\n'.join(lines)
2553
2554     def __str__(self):
2555         """return "detailed" format"""
2556         lines = ['Request: #%s\n' % self.reqid]
2557         for action in self.actions:
2558             tmpl = '  %(type)-13s %(source)s %(target)s'
2559             if action.type == 'delete':
2560                 # remove 1 whitespace because source is empty
2561                 tmpl = '  %(type)-12s %(source)s %(target)s'
2562             lines.append(tmpl % Request.format_action(action, show_srcupdate=True))
2563         lines.append('\n\nMessage:')
2564         if self.description:
2565             lines.append(self.description)
2566         else:
2567             lines.append('<no message>')
2568         if self.state:
2569             lines.append('\nState:   %-10s %-12s %s' % (self.state.name, self.state.when, self.state.who))
2570             lines.append('Comment: %s' % (self.state.comment or '<no comment>'))
2571
2572         indent = '\n         '
2573         tmpl = '%(state)-10s %(by)-50s %(when)-12s %(who)-20s  %(comment)s'
2574         reviews = []
2575         for review in reversed(self.reviews):
2576             d = {'state': review.state}
2577             if review.by_user:
2578               d['by'] = "User: " + review.by_user
2579             if review.by_group:
2580               d['by'] = "Group: " + review.by_group
2581             if review.by_package:
2582               d['by'] = "Package: " + review.by_project + "/" + review.by_package 
2583             elif review.by_project:
2584               d['by'] = "Project: " + review.by_project
2585             d['when'] = review.when or ''
2586             d['who'] = review.who or ''
2587             d['comment'] = review.comment or ''
2588             reviews.append(tmpl % d)
2589         if reviews:
2590             lines.append('\nReview:  %s' % indent.join(reviews))
2591
2592         tmpl = '%(name)-10s %(when)-12s %(who)s'
2593         histories = []
2594         for hist in reversed(self.statehistory):
2595             d = {'name': hist.name, 'when': hist.when,
2596                 'who': hist.who}
2597             histories.append(tmpl % d)
2598         if histories:
2599             lines.append('\nHistory: %s' % indent.join(histories))
2600
2601         return '\n'.join(lines)
2602
2603     def __cmp__(self, other):
2604         return cmp(int(self.reqid), int(other.reqid))
2605
2606     def create(self, apiurl):
2607         """create a new request"""
2608         u = makeurl(apiurl, ['request'], query='cmd=create')
2609         f = http_POST(u, data=self.to_str())
2610         root = ET.fromstring(f.read())
2611         self.read(root)
2612
2613 def shorttime(t):
2614     """format time as Apr 02 18:19
2615     or                Apr 02  2005
2616     depending on whether it is in the current year
2617     """
2618     import time
2619
2620     if time.localtime()[0] == time.localtime(t)[0]:
2621         # same year
2622         return time.strftime('%b %d %H:%M',time.localtime(t))
2623     else:
2624         return time.strftime('%b %d  %Y',time.localtime(t))
2625
2626
2627 def is_project_dir(d):
2628     global store
2629
2630     return os.path.exists(os.path.join(d, store, '_project')) and not \
2631            os.path.exists(os.path.join(d, store, '_package'))
2632
2633
2634 def is_package_dir(d):
2635     global store
2636
2637     return os.path.exists(os.path.join(d, store, '_project')) and \
2638            os.path.exists(os.path.join(d, store, '_package'))
2639
2640 def parse_disturl(disturl):
2641     """Parse a disturl, returns tuple (apiurl, project, source, repository,
2642     revision), else raises an oscerr.WrongArgs exception
2643     """
2644
2645     global DISTURL_RE
2646
2647     m = DISTURL_RE.match(disturl)
2648     if not m:
2649         raise oscerr.WrongArgs("`%s' does not look like disturl" % disturl)
2650
2651     apiurl = m.group('apiurl')
2652     if apiurl.split('.')[0] != 'api':
2653         apiurl = 'https://api.' + ".".join(apiurl.split('.')[1:])
2654     return (apiurl, m.group('project'), m.group('source'), m.group('repository'), m.group('revision'))
2655
2656 def parse_buildlogurl(buildlogurl):
2657     """Parse a build log url, returns a tuple (apiurl, project, package,
2658     repository, arch), else raises oscerr.WrongArgs exception"""
2659
2660     global BUILDLOGURL_RE
2661
2662     m = BUILDLOGURL_RE.match(buildlogurl)
2663     if not m:
2664         raise oscerr.WrongArgs('\'%s\' does not look like url with a build log' % buildlogurl)
2665
2666     return (m.group('apiurl'), m.group('project'), m.group('package'), m.group('repository'), m.group('arch'))
2667
2668 def slash_split(l):
2669     """Split command line arguments like 'foo/bar' into 'foo' 'bar'.
2670     This is handy to allow copy/paste a project/package combination in this form.
2671
2672     Trailing slashes are removed before the split, because the split would
2673     otherwise give an additional empty string.
2674     """
2675     r = []
2676     for i in l:
2677         i = i.rstrip('/')
2678         r += i.split('/')
2679     return r
2680
2681 def expand_proj_pack(args, idx=0, howmany=0):
2682     """looks for occurance of '.' at the position idx.
2683     If howmany is 2, both proj and pack are expanded together
2684     using the current directory, or none of them, if not possible.
2685     If howmany is 0, proj is expanded if possible, then, if there
2686     is no idx+1 element in args (or args[idx+1] == '.'), pack is also
2687     expanded, if possible.
2688     If howmany is 1, only proj is expanded if possible.
2689
2690     If args[idx] does not exists, an implicit '.' is assumed.
2691     if not enough elements up to idx exist, an error is raised.
2692
2693     See also parseargs(args), slash_split(args), findpacs(args)
2694     All these need unification, somehow.
2695     """
2696
2697     # print args,idx,howmany
2698
2699     if len(args) < idx:
2700         raise oscerr.WrongArgs('not enough argument, expected at least %d' % idx)
2701
2702     if len(args) == idx:
2703         args += '.'
2704     if args[idx+0] == '.':
2705         if howmany == 0 and len(args) > idx+1:
2706             if args[idx+1] == '.':
2707                 # we have two dots.
2708                 # remove one dot and make sure to expand both proj and pack
2709                 args.pop(idx+1)
2710                 howmany = 2
2711             else:
2712                 howmany = 1
2713         # print args,idx,howmany
2714
2715         args[idx+0] = store_read_project('.')
2716         if howmany == 0:
2717             try:
2718                 package = store_read_package('.')
2719                 args.insert(idx+1, package)
2720             except:
2721                 pass
2722         elif howmany == 2:
2723             package = store_read_package('.')
2724             args.insert(idx+1, package)
2725     return args
2726
2727
2728 def findpacs(files, progress_obj=None):
2729     """collect Package objects belonging to the given files
2730     and make sure each Package is returned only once"""
2731     pacs = []
2732     for f in files:
2733         p = filedir_to_pac(f, progress_obj)
2734         known = None
2735         for i in pacs:
2736             if i.name == p.name:
2737                 known = i
2738                 break
2739         if known:
2740             i.merge(p)
2741         else:
2742             pacs.append(p)
2743     return pacs
2744
2745
2746 def filedir_to_pac(f, progress_obj=None):
2747     """Takes a working copy path, or a path to a file inside a working copy,
2748     and returns a Package object instance
2749
2750     If the argument was a filename, add it onto the "todo" list of the Package """
2751
2752     if os.path.isdir(f):
2753         wd = f
2754         p = Package(wd, progress_obj=progress_obj)
2755     else:
2756         wd = os.path.dirname(f) or os.curdir
2757         p = Package(wd, progress_obj=progress_obj)
2758         p.todo = [ os.path.basename(f) ]
2759     return p
2760
2761
2762 def read_filemeta(dir):
2763     global store
2764
2765     msg = '\'%s\' is not a valid working copy.' % dir
2766     filesmeta = os.path.join(dir, store, '_files')
2767     if not is_package_dir(dir):
2768         raise oscerr.NoWorkingCopy(msg)
2769     if not os.path.isfile(filesmeta):
2770         raise oscerr.NoWorkingCopy('%s (%s does not exist)' % (msg, filesmeta))
2771
2772     try:
2773         r = ET.parse(filesmeta)
2774     except SyntaxError, e:
2775         raise oscerr.NoWorkingCopy('%s\nWhen parsing .osc/_files, the following error was encountered:\n%s' % (msg, e))
2776     return r
2777
2778 def store_readlist(dir, name):
2779     global store
2780
2781     r = []
2782     if os.path.exists(os.path.join(dir, store, name)):
2783         r = [line.strip() for line in open(os.path.join(dir, store, name), 'r')]
2784     return r
2785
2786 def read_tobeadded(dir):
2787     return store_readlist(dir, '_to_be_added')
2788
2789 def read_tobedeleted(dir):
2790     return store_readlist(dir, '_to_be_deleted')
2791
2792 def read_sizelimit(dir):
2793     global store
2794
2795     r = None
2796     fname = os.path.join(dir, store, '_size_limit')
2797
2798     if os.path.exists(fname):
2799         r = open(fname).readline().strip()
2800
2801     if r is None or not r.isdigit():
2802         return None
2803     return int(r)
2804
2805 def read_inconflict(dir):
2806     return store_readlist(dir, '_in_conflict')
2807
2808 def parseargs(list_of_args):
2809     """Convenience method osc's commandline argument parsing.
2810
2811     If called with an empty tuple (or list), return a list containing the current directory.
2812     Otherwise, return a list of the arguments."""
2813     if list_of_args:
2814         return list(list_of_args)
2815     else:
2816         return [os.curdir]
2817
2818
2819 def statfrmt(statusletter, filename):
2820     return '%s    %s' % (statusletter, filename)
2821
2822
2823 def pathjoin(a, *p):
2824     """Join two or more pathname components, inserting '/' as needed. Cut leading ./"""
2825     path = os.path.join(a, *p)
2826     if path.startswith('./'):
2827         path = path[2:]
2828     return path
2829
2830
2831 def makeurl(baseurl, l, query=[]):
2832     """Given a list of path compoments, construct a complete URL.
2833
2834     Optional parameters for a query string can be given as a list, as a
2835     dictionary, or as an already assembled string.
2836     In case of a dictionary, the parameters will be urlencoded by this
2837     function. In case of a list not -- this is to be backwards compatible.
2838     """
2839
2840     if conf.config['verbose'] > 1:
2841         print 'makeurl:', baseurl, l, query
2842
2843     if type(query) == type(list()):
2844         query = '&'.join(query)
2845     elif type(query) == type(dict()):
2846         query = urlencode(query)
2847
2848     scheme, netloc = urlsplit(baseurl)[0:2]
2849     return urlunsplit((scheme, netloc, '/'.join(l), query, ''))
2850
2851
2852 def http_request(method, url, headers={}, data=None, file=None, timeout=100):
2853     """wrapper around urllib2.urlopen for error handling,
2854     and to support additional (PUT, DELETE) methods"""
2855
2856     filefd = None
2857
2858     if conf.config['http_debug']:
2859         print >>sys.stderr, '\n\n--', method, url
2860
2861     if method == 'POST' and not file and not data:
2862         # adding data to an urllib2 request transforms it into a POST
2863         data = ''
2864
2865     req = urllib2.Request(url)
2866     api_host_options = {}
2867     if conf.is_known_apiurl(url):
2868         # ok no external request
2869         urllib2.install_opener(conf._build_opener(url))
2870         api_host_options = conf.get_apiurl_api_host_options(url)
2871         for header, value in api_host_options['http_headers']:
2872             req.add_header(header, value)
2873
2874     req.get_method = lambda: method
2875
2876     # POST requests are application/x-www-form-urlencoded per default
2877     # since we change the request into PUT, we also need to adjust the content type header
2878     if method == 'PUT' or (method == 'POST' and data):
2879         req.add_header('Content-Type', 'application/octet-stream')
2880
2881     if type(headers) == type({}):
2882         for i in headers.keys():
2883             print headers[i]
2884             req.add_header(i, headers[i])
2885
2886     if file and not data:
2887         size = os.path.getsize(file)
2888         if size < 1024*512:
2889             data = open(file, 'rb').read()
2890         else:
2891             import mmap
2892             filefd = open(file, 'rb')
2893             try:
2894                 if sys.platform[:3] != 'win':
2895                     data = mmap.mmap(filefd.fileno(), os.path.getsize(file), mmap.MAP_SHARED, mmap.PROT_READ)
2896                 else:
2897                     data = mmap.mmap(filefd.fileno(), os.path.getsize(file))
2898                 data = buffer(data)
2899             except EnvironmentError, e:
2900                 if e.errno == 19:
2901                     sys.exit('\n\n%s\nThe file \'%s\' could not be memory mapped. It is ' \
2902                              '\non a filesystem which does not support this.' % (e, file))
2903                 elif hasattr(e, 'winerror') and e.winerror == 5:
2904                     # falling back to the default io
2905                     data = open(file, 'rb').read()
2906                 else:
2907                     raise
2908
2909     if conf.config['debug']: print >>sys.stderr, method, url
2910
2911     old_timeout = socket.getdefaulttimeout()
2912     # XXX: dirty hack as timeout doesn't work with python-m2crypto
2913     if old_timeout != timeout and not api_host_options.get('sslcertck'):
2914         socket.setdefaulttimeout(timeout)
2915     try:
2916         fd = urllib2.urlopen(req, data=data)
2917     finally:
2918         if old_timeout != timeout and not api_host_options.get('sslcertck'):
2919             socket.setdefaulttimeout(old_timeout)
2920         if hasattr(conf.cookiejar, 'save'):
2921             conf.cookiejar.save(ignore_discard=True)
2922
2923     if filefd: filefd.close()
2924
2925     return fd
2926
2927
2928 def http_GET(*args, **kwargs):    return http_request('GET', *args, **kwargs)
2929 def http_POST(*args, **kwargs):   return http_request('POST', *args, **kwargs)
2930 def http_PUT(*args, **kwargs):    return http_request('PUT', *args, **kwargs)
2931 def http_DELETE(*args, **kwargs): return http_request('DELETE', *args, **kwargs)
2932
2933
2934 def check_store_version(dir):
2935     global store
2936
2937     versionfile = os.path.join(dir, store, '_osclib_version')
2938     try:
2939         v = open(versionfile).read().strip()
2940     except:
2941         v = ''
2942
2943     if v == '':
2944         msg = 'Error: "%s" is not an osc package working copy.' % os.path.abspath(dir)
2945         if os.path.exists(os.path.join(dir, '.svn')):
2946             msg = msg + '\nTry svn instead of osc.'
2947         raise oscerr.NoWorkingCopy(msg)
2948
2949     if v != __store_version__:
2950         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']:
2951             # version is fine, no migration needed
2952             f = open(versionfile, 'w')
2953             f.write(__store_version__ + '\n')
2954             f.close()
2955             return
2956         msg = 'The osc metadata of your working copy "%s"' % dir
2957         msg += '\nhas __store_version__ = %s, but it should be %s' % (v, __store_version__)
2958         msg += '\nPlease do a fresh checkout or update your client. Sorry about the inconvenience.'
2959         raise oscerr.WorkingCopyWrongVersion, msg
2960
2961
2962 def meta_get_packagelist(apiurl, prj, deleted=None):
2963
2964     query = {}
2965     if deleted:
2966        query['deleted'] = 1
2967
2968     u = makeurl(apiurl, ['source', prj], query)
2969     f = http_GET(u)
2970     root = ET.parse(f).getroot()
2971     return [ node.get('name') for node in root.findall('entry') ]
2972
2973
2974 def meta_get_filelist(apiurl, prj, package, verbose=False, expand=False, revision=None):
2975     """return a list of file names,
2976     or a list File() instances if verbose=True"""
2977
2978     query = {}
2979     if expand:
2980         query['expand'] = 1
2981     if revision:
2982         query['rev'] = revision
2983     else:
2984         query['rev'] = 'latest'
2985
2986     u = makeurl(apiurl, ['source', prj, package], query=query)
2987     f = http_GET(u)
2988     root = ET.parse(f).getroot()
2989
2990     if not verbose:
2991         return [ node.get('name') for node in root.findall('entry') ]
2992
2993     else:
2994         l = []
2995         # rev = int(root.get('rev'))    # don't force int. also allow srcmd5 here.
2996         rev = root.get('rev')
2997         for node in root.findall('entry'):
2998             f = File(node.get('name'),
2999                      node.get('md5'),
3000                      int(node.get('size')),
3001                      int(node.get('mtime')))
3002             f.rev = rev
3003             l.append(f)
3004         return l
3005
3006
3007 def meta_get_project_list(apiurl, deleted=None):
3008     query = {}
3009     if deleted:
3010         query['deleted'] = 1
3011
3012     u = makeurl(apiurl, ['source'], query)
3013     f = http_GET(u)
3014     root = ET.parse(f).getroot()
3015     return sorted([ node.get('name') for node in root if node.get('name')])
3016
3017
3018 def show_project_meta(apiurl, prj):
3019     url = makeurl(apiurl, ['source', prj, '_meta'])
3020     f = http_GET(url)
3021     return f.readlines()
3022
3023
3024 def show_project_conf(apiurl, prj):
3025     url = makeurl(apiurl, ['source', prj, '_config'])
3026     f = http_GET(url)
3027     return f.readlines()
3028
3029
3030 def show_package_trigger_reason(apiurl, prj, pac, repo, arch):
3031     url = makeurl(apiurl, ['build', prj, repo, arch, pac, '_reason'])
3032     try:
3033         f = http_GET(url)
3034         return f.read()
3035     except urllib2.HTTPError, e:
3036         e.osc_msg = 'Error getting trigger reason for project \'%s\' package \'%s\'' % (prj, pac)
3037         raise
3038
3039
3040 def show_package_meta(apiurl, prj, pac, meta=False):
3041     query = {}
3042     if meta:
3043         query['meta'] = 1
3044
3045     # packages like _pattern and _project do not have a _meta file
3046     if pac.startswith('_pattern') or pac.startswith('_project'):
3047         return ""
3048
3049     url = makeurl(apiurl, ['source', prj, pac, '_meta'], query)
3050     try:
3051         f = http_GET(url)
3052         return f.readlines()
3053     except urllib2.HTTPError, e:
3054         e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
3055         raise
3056
3057
3058 def show_attribute_meta(apiurl, prj, pac, subpac, attribute, with_defaults, with_project):
3059     path=[]
3060     path.append('source')
3061     path.append(prj)
3062     if pac:
3063         path.append(pac)
3064     if pac and subpac:
3065         path.append(subpac)
3066     path.append('_attribute')
3067     if attribute:
3068         path.append(attribute)
3069     query=[]
3070     if with_defaults:
3071         query.append("with_default=1")
3072     if with_project:
3073         query.append("with_project=1")
3074     url = makeurl(apiurl, path, query)
3075     try:
3076         f = http_GET(url)
3077         return f.readlines()
3078     except urllib2.HTTPError, e:
3079         e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
3080         raise
3081
3082
3083 def show_develproject(apiurl, prj, pac, xml_node=False):
3084     m = show_package_meta(apiurl, prj, pac)
3085     node = ET.fromstring(''.join(m)).find('devel')
3086     if not node is None:
3087         if xml_node:
3088             return node
3089         return node.get('project')
3090     return None
3091
3092
3093 def show_package_disabled_repos(apiurl, prj, pac):
3094     m = show_package_meta(apiurl, prj, pac)
3095     #FIXME: don't work if all repos of a project are disabled and only some are enabled since <disable/> is empty
3096     try:
3097         root = ET.fromstring(''.join(m))
3098         elm = root.find('build')
3099         r = [ node.get('repository') for node in elm.findall('disable')]
3100         return r
3101     except:
3102         return None
3103
3104
3105 def show_pattern_metalist(apiurl, prj):
3106     url = makeurl(apiurl, ['source', prj, '_pattern'])
3107     try:
3108         f = http_GET(url)
3109         tree = ET.parse(f)
3110     except urllib2.HTTPError, e:
3111         e.osc_msg = 'show_pattern_metalist: Error getting pattern list for project \'%s\'' % prj
3112         raise
3113     r = [ node.get('name') for node in tree.getroot() ]
3114     r.sort()
3115     return r
3116
3117
3118 def show_pattern_meta(apiurl, prj, pattern):
3119     url = makeurl(apiurl, ['source', prj, '_pattern', pattern])
3120     try:
3121         f = http_GET(url)
3122         return f.readlines()
3123     except urllib2.HTTPError, e:
3124         e.osc_msg = 'show_pattern_meta: Error getting pattern \'%s\' for project \'%s\'' % (pattern, prj)
3125         raise
3126
3127
3128 class metafile:
3129     """metafile that can be manipulated and is stored back after manipulation."""
3130     def __init__(self, url, input, change_is_required=False, file_ext='.xml'):
3131         import tempfile
3132
3133         self.url = url
3134         self.change_is_required = change_is_required
3135         (fd, self.filename) = tempfile.mkstemp(prefix = 'osc_metafile.', suffix = file_ext)
3136         f = os.fdopen(fd, 'w')
3137         f.write(''.join(input))
3138         f.close()
3139         self.hash_orig = dgst(self.filename)
3140
3141     def sync(self):
3142         if self.change_is_required and self.hash_orig == dgst(self.filename):
3143             print 'File unchanged. Not saving.'
3144             os.unlink(self.filename)
3145             return
3146
3147         print 'Sending meta data...'
3148         # don't do any exception handling... it's up to the caller what to do in case
3149         # of an exception
3150         http_PUT(self.url, file=self.filename)
3151         os.unlink(self.filename)
3152         print 'Done.'
3153
3154     def edit(self):
3155         try:
3156             while 1:
3157                 run_editor(self.filename)
3158                 try:
3159                     self.sync()
3160                     break
3161                 except urllib2.HTTPError, e:
3162                     error_help = "%d" % e.code
3163                     if e.headers.get('X-Opensuse-Errorcode'):
3164                         error_help = "%s (%d)" % (e.headers.get('X-Opensuse-Errorcode'), e.code)
3165
3166                     print >>sys.stderr, 'BuildService API error:', error_help
3167                     # examine the error - we can't raise an exception because we might want
3168                     # to try again
3169                     data = e.read()
3170                     if '<summary>' in data:
3171                         print >>sys.stderr, data.split('<summary>')[1].split('</summary>')[0]
3172                     ri = raw_input('Try again? ([y/N]): ')
3173                     if ri not in ['y', 'Y']:
3174                         break
3175         finally:
3176             self.discard()
3177
3178     def discard(self):
3179         if os.path.exists(self.filename):
3180             print 'discarding %s' % self.filename
3181             os.unlink(self.filename)
3182
3183
3184 # different types of metadata
3185 metatypes = { 'prj':     { 'path': 'source/%s/_meta',
3186                            'template': new_project_templ,
3187                            'file_ext': '.xml'
3188                          },
3189               'pkg':     { 'path'     : 'source/%s/%s/_meta',
3190                            'template': new_package_templ,
3191                            'file_ext': '.xml'
3192                          },
3193               'attribute':     { 'path'     : 'source/%s/%s/_meta',
3194                            'template': new_attribute_templ,
3195                            'file_ext': '.xml'
3196                          },
3197               'prjconf': { 'path': 'source/%s/_config',
3198                            'template': '',
3199                            'file_ext': '.txt'
3200                          },
3201               'user':    { 'path': 'person/%s',
3202                            'template': new_user_template,
3203                            'file_ext': '.xml'
3204                          },
3205               'pattern': { 'path': 'source/%s/_pattern/%s',
3206                            'template': new_pattern_template,
3207                            'file_ext': '.xml'
3208                          },
3209             }
3210
3211 def meta_exists(metatype,
3212                 path_args=None,
3213                 template_args=None,
3214                 create_new=True,
3215                 apiurl=None):
3216
3217     global metatypes
3218
3219     if not apiurl:
3220         apiurl = conf.config['apiurl']
3221     url = make_meta_url(metatype, path_args, apiurl)
3222     try:
3223         data = http_GET(url).readlines()
3224     except urllib2.HTTPError, e:
3225         if e.code == 404 and create_new:
3226             data = metatypes[metatype]['template']
3227             if template_args:
3228                 data = StringIO(data % template_args).readlines()
3229         else:
3230             raise e
3231
3232     return data
3233
3234 def make_meta_url(metatype, path_args=None, apiurl=None, force=False):
3235     global metatypes
3236
3237     if not apiurl:
3238         apiurl = conf.config['apiurl']
3239     if metatype not in metatypes.keys():
3240         raise AttributeError('make_meta_url(): Unknown meta type \'%s\'' % metatype)
3241     path = metatypes[metatype]['path']
3242
3243     if path_args:
3244         path = path % path_args
3245
3246     query = {}
3247     if force:
3248         query = { 'force': '1' }
3249
3250     return makeurl(apiurl, [path], query)
3251
3252
3253 def edit_meta(metatype,
3254               path_args=None,
3255               data=None,
3256               template_args=None,
3257               edit=False,
3258               force=False,
3259               change_is_required=False,
3260               apiurl=None):
3261
3262     global metatypes
3263
3264     if not apiurl:
3265         apiurl = conf.config['apiurl']
3266     if not data:
3267         data = meta_exists(metatype,
3268                            path_args,
3269                            template_args,
3270                            create_new = metatype != 'prjconf', # prjconf always exists, 404 => unknown prj
3271                            apiurl=apiurl)
3272
3273     if edit:
3274         change_is_required = True
3275
3276     url = make_meta_url(metatype, path_args, apiurl, force)
3277     f=metafile(url, data, change_is_required, metatypes[metatype]['file_ext'])
3278
3279     if edit:
3280         f.edit()
3281     else:
3282         f.sync()
3283
3284
3285 def show_files_meta(apiurl, prj, pac, revision=None, expand=False, linkrev=None, linkrepair=False, meta=False):
3286     query = {}
3287     if revision:
3288         query['rev'] = revision
3289     else:
3290         query['rev'] = 'latest'
3291     if linkrev:
3292         query['linkrev'] = linkrev
3293     elif conf.config['linkcontrol']:
3294         query['linkrev'] = 'base'
3295     if meta:
3296         query['meta'] = 1
3297     if expand:
3298         query['expand'] = 1
3299     if linkrepair:
3300         query['emptylink'] = 1
3301     f = http_GET(makeurl(apiurl, ['source', prj, pac], query=query))
3302     return f.read()
3303
3304 def show_upstream_srcmd5(apiurl, prj, pac, expand=False, revision=None, meta=False, include_service_files=False):
3305     m = show_files_meta(apiurl, prj, pac, expand=expand, revision=revision, meta=meta)
3306     et = ET.fromstring(''.join(m))
3307     if include_service_files:
3308         try:
3309             if et.find('serviceinfo') and et.find('serviceinfo').get('xsrcmd5'):
3310                 return et.find('serviceinfo').get('xsrcmd5')
3311         except:
3312             pass
3313     return et.get('srcmd5')
3314
3315
3316 def show_upstream_xsrcmd5(apiurl, prj, pac, revision=None, linkrev=None, linkrepair=False, meta=False, include_service_files=False):
3317     m = show_files_meta(apiurl, prj, pac, revision=revision, linkrev=linkrev, linkrepair=linkrepair, meta=meta, expand=include_service_files)
3318     et =