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