- tag 0.132.3
[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.3'
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         if self.findfilebyname("_service"):
1332             print 'Waiting for server side source service run',
1333             u = makeurl(self.apiurl, ['source', self.prjname, self.name])
1334             while 1:
1335                 f = http_GET(u)
1336                 sfilelist = ET.parse(f).getroot()
1337                 s = sfilelist.find('serviceinfo')
1338                 if s != None and s.get('code') == "running":
1339                    sys.stdout.write('.')
1340                    sys.stdout.flush()
1341                 else:
1342                    break
1343             print ""
1344             rev=self.latest_rev()
1345             self.update(rev=rev)
1346             
1347
1348     def __write_storelist(self, name, data):
1349         if len(data) == 0:
1350             try:
1351                 os.unlink(os.path.join(self.storedir, name))
1352             except:
1353                 pass
1354         else:
1355             store_write_string(self.absdir, name, '%s\n' % '\n'.join(data))
1356
1357     def write_conflictlist(self):
1358         self.__write_storelist('_in_conflict', self.in_conflict)
1359
1360     def updatefile(self, n, revision, mtime=None):
1361         filename = os.path.join(self.dir, n)
1362         storefilename = os.path.join(self.storedir, n)
1363         origfile_tmp = os.path.join(self.storedir, '_in_update', '%s.copy' % n)
1364         origfile = os.path.join(self.storedir, '_in_update', n)
1365         if os.path.isfile(filename):
1366             shutil.copyfile(filename, origfile_tmp)
1367             os.rename(origfile_tmp, origfile)
1368         else:
1369             origfile = None
1370
1371         get_source_file(self.apiurl, self.prjname, self.name, n, targetfilename=storefilename,
1372                 revision=revision, progress_obj=self.progress_obj, mtime=mtime, meta=self.meta)
1373
1374         shutil.copyfile(storefilename, filename)
1375         if not origfile is None:
1376             os.unlink(origfile)
1377
1378     def mergefile(self, n, revision, mtime=None):
1379         filename = os.path.join(self.dir, n)
1380         storefilename = os.path.join(self.storedir, n)
1381         myfilename = os.path.join(self.dir, n + '.mine')
1382         upfilename = os.path.join(self.dir, n + '.r' + self.rev)
1383         origfile_tmp = os.path.join(self.storedir, '_in_update', '%s.copy' % n)
1384         origfile = os.path.join(self.storedir, '_in_update', n)
1385         shutil.copyfile(filename, origfile_tmp)
1386         os.rename(origfile_tmp, origfile)
1387         os.rename(filename, myfilename)
1388
1389         get_source_file(self.apiurl, self.prjname, self.name, n,
1390                         revision=revision, targetfilename=upfilename,
1391                         progress_obj=self.progress_obj, mtime=mtime, meta=self.meta)
1392
1393         if binary_file(myfilename) or binary_file(upfilename):
1394             # don't try merging
1395             shutil.copyfile(upfilename, filename)
1396             shutil.copyfile(upfilename, storefilename)
1397             os.unlink(origfile)
1398             self.in_conflict.append(n)
1399             self.write_conflictlist()
1400             return 'C'
1401         else:
1402             # try merging
1403             # diff3 OPTIONS... MINE OLDER YOURS
1404             merge_cmd = 'diff3 -m -E %s %s %s > %s' % (myfilename, storefilename, upfilename, filename)
1405             # we would rather use the subprocess module, but it is not availablebefore 2.4
1406             ret = subprocess.call(merge_cmd, shell=True)
1407
1408             #   "An exit status of 0 means `diff3' was successful, 1 means some
1409             #   conflicts were found, and 2 means trouble."
1410             if ret == 0:
1411                 # merge was successful... clean up
1412                 shutil.copyfile(upfilename, storefilename)
1413                 os.unlink(upfilename)
1414                 os.unlink(myfilename)
1415                 os.unlink(origfile)
1416                 return 'G'
1417             elif ret == 1:
1418                 # unsuccessful merge
1419                 shutil.copyfile(upfilename, storefilename)
1420                 os.unlink(origfile)
1421                 self.in_conflict.append(n)
1422                 self.write_conflictlist()
1423                 return 'C'
1424             else:
1425                 raise oscerr.ExtRuntimeError('diff3 failed with exit code: %s' % ret, merge_cmd)
1426
1427     def update_local_filesmeta(self, revision=None):
1428         """
1429         Update the local _files file in the store.
1430         It is replaced with the version pulled from upstream.
1431         """
1432         meta = self.get_files_meta(revision=revision)
1433         store_write_string(self.absdir, '_files', meta + '\n')
1434
1435     def get_files_meta(self, revision='latest', skip_service=True):
1436         fm = show_files_meta(self.apiurl, self.prjname, self.name, revision=revision, meta=self.meta)
1437         # look for "too large" files according to size limit and mark them
1438         root = ET.fromstring(fm)
1439         for e in root.findall('entry'):
1440             size = e.get('size')
1441             if size and self.size_limit and int(size) > self.size_limit \
1442                 or skip_service and (e.get('name').startswith('_service:') or e.get('name').startswith('_service_')):
1443                 e.set('skipped', 'true')
1444         return ET.tostring(root)
1445
1446     def update_datastructs(self):
1447         """
1448         Update the internal data structures if the local _files
1449         file has changed (e.g. update_local_filesmeta() has been
1450         called).
1451         """
1452         import fnmatch
1453         files_tree = read_filemeta(self.dir)
1454         files_tree_root = files_tree.getroot()
1455
1456         self.rev = files_tree_root.get('rev')
1457         self.srcmd5 = files_tree_root.get('srcmd5')
1458
1459         self.linkinfo = Linkinfo()
1460         self.linkinfo.read(files_tree_root.find('linkinfo'))
1461
1462         self.filenamelist = []
1463         self.filelist = []
1464         self.skipped = []
1465         for node in files_tree_root.findall('entry'):
1466             try:
1467                 f = File(node.get('name'),
1468                          node.get('md5'),
1469                          int(node.get('size')),
1470                          int(node.get('mtime')))
1471                 if node.get('skipped'):
1472                     self.skipped.append(f.name)
1473                     f.skipped = True
1474             except:
1475                 # okay, a very old version of _files, which didn't contain any metadata yet...
1476                 f = File(node.get('name'), '', 0, 0)
1477             self.filelist.append(f)
1478             self.filenamelist.append(f.name)
1479
1480         self.to_be_added = read_tobeadded(self.absdir)
1481         self.to_be_deleted = read_tobedeleted(self.absdir)
1482         self.in_conflict = read_inconflict(self.absdir)
1483         self.linkrepair = os.path.isfile(os.path.join(self.storedir, '_linkrepair'))
1484         self.size_limit = read_sizelimit(self.dir)
1485         self.meta = self.ismetamode()
1486
1487         # gather unversioned files, but ignore some stuff
1488         self.excluded = []
1489         for i in os.listdir(self.dir):
1490             for j in conf.config['exclude_glob']:
1491                 if fnmatch.fnmatch(i, j):
1492                     self.excluded.append(i)
1493                     break
1494         self.filenamelist_unvers = [ i for i in os.listdir(self.dir)
1495                                      if i not in self.excluded
1496                                      if i not in self.filenamelist ]
1497
1498     def islink(self):
1499         """tells us if the package is a link (has 'linkinfo').
1500         A package with linkinfo is a package which links to another package.
1501         Returns True if the package is a link, otherwise False."""
1502         return self.linkinfo.islink()
1503
1504     def isexpanded(self):
1505         """tells us if the package is a link which is expanded.
1506         Returns True if the package is expanded, otherwise False."""
1507         return self.linkinfo.isexpanded()
1508
1509     def islinkrepair(self):
1510         """tells us if we are repairing a broken source link."""
1511         return self.linkrepair
1512
1513     def ispulled(self):
1514         """tells us if we have pulled a link."""
1515         return os.path.isfile(os.path.join(self.storedir, '_pulled'))
1516
1517     def isfrozen(self):
1518         """tells us if the link is frozen."""
1519         return os.path.isfile(os.path.join(self.storedir, '_frozenlink'))
1520
1521     def ismetamode(self):
1522         """tells us if the package is in meta mode"""
1523         return os.path.isfile(os.path.join(self.storedir, '_meta_mode'))
1524
1525     def get_pulled_srcmd5(self):
1526         pulledrev = None
1527         for line in open(os.path.join(self.storedir, '_pulled'), 'r'):
1528             pulledrev = line.strip()
1529         return pulledrev
1530
1531     def haslinkerror(self):
1532         """
1533         Returns True if the link is broken otherwise False.
1534         If the package is not a link it returns False.
1535         """
1536         return self.linkinfo.haserror()
1537
1538     def linkerror(self):
1539         """
1540         Returns an error message if the link is broken otherwise None.
1541         If the package is not a link it returns None.
1542         """
1543         return self.linkinfo.error
1544
1545     def update_local_pacmeta(self):
1546         """
1547         Update the local _meta file in the store.
1548         It is replaced with the version pulled from upstream.
1549         """
1550         meta = ''.join(show_package_meta(self.apiurl, self.prjname, self.name))
1551         store_write_string(self.absdir, '_meta', meta + '\n')
1552
1553     def findfilebyname(self, n):
1554         for i in self.filelist:
1555             if i.name == n:
1556                 return i
1557
1558     def get_status(self, excluded=False, *exclude_states):
1559         global store
1560         todo = self.todo
1561         if not todo:
1562             todo = self.filenamelist + self.to_be_added + \
1563                 [i for i in self.filenamelist_unvers if not os.path.isdir(os.path.join(self.absdir, i))]
1564             if excluded:
1565                 todo.extend([i for i in self.excluded if i != store])
1566             todo = set(todo)
1567         res = []
1568         for fname in sorted(todo):
1569             st = self.status(fname)
1570             if not st in exclude_states:
1571                 res.append((st, fname))
1572         return res
1573
1574     def status(self, n):
1575         """
1576         status can be:
1577
1578          file  storefile  file present  STATUS
1579         exists  exists      in _files
1580
1581           x       -            -        'A' and listed in _to_be_added
1582           x       x            -        'R' and listed in _to_be_added
1583           x       x            x        ' ' if digest differs: 'M'
1584                                             and if in conflicts file: 'C'
1585           x       -            -        '?'
1586           -       x            x        'D' and listed in _to_be_deleted
1587           x       x            x        'D' and listed in _to_be_deleted (e.g. if deleted file was modified)
1588           x       x            x        'C' and listed in _in_conflict
1589           x       -            x        'S' and listed in self.skipped
1590           -       -            x        'S' and listed in self.skipped
1591           -       x            x        '!'
1592           -       -            -        NOT DEFINED
1593
1594         """
1595
1596         known_by_meta = False
1597         exists = False
1598         exists_in_store = False
1599         if n in self.filenamelist:
1600             known_by_meta = True
1601         if os.path.exists(os.path.join(self.absdir, n)):
1602             exists = True
1603         if os.path.exists(os.path.join(self.storedir, n)):
1604             exists_in_store = True
1605
1606         if n in self.to_be_deleted:
1607             state = 'D'
1608         elif n in self.in_conflict:
1609             state = 'C'
1610         elif n in self.skipped:
1611             state = 'S'
1612         elif n in self.to_be_added and exists and exists_in_store:
1613             state = 'R'
1614         elif n in self.to_be_added and exists:
1615             state = 'A'
1616         elif exists and exists_in_store and known_by_meta:
1617             if dgst(os.path.join(self.absdir, n)) != self.findfilebyname(n).md5:
1618                 state = 'M'
1619             else:
1620                 state = ' '
1621         elif n in self.to_be_added and not exists:
1622             state = '!'
1623         elif not exists and exists_in_store and known_by_meta and not n in self.to_be_deleted:
1624             state = '!'
1625         elif exists and not exists_in_store and not known_by_meta:
1626             state = '?'
1627         elif not exists_in_store and known_by_meta:
1628             # XXX: this codepath shouldn't be reached (we restore the storefile
1629             #      in update_datastructs)
1630             raise oscerr.PackageInternalError(self.prjname, self.name,
1631                 'error: file \'%s\' is known by meta but no storefile exists.\n'
1632                 'This might be caused by an old wc format. Please backup your current\n'
1633                 'wc and checkout the package again. Afterwards copy all files (except the\n'
1634                 '.osc/ dir) into the new package wc.' % n)
1635         else:
1636             # this case shouldn't happen (except there was a typo in the filename etc.)
1637             raise oscerr.OscIOError(None, 'osc: \'%s\' is not under version control' % n)
1638
1639         return state
1640
1641     def get_diff(self, revision=None, ignoreUnversioned=False):
1642         import tempfile
1643         diff_hdr = 'Index: %s\n'
1644         diff_hdr += '===================================================================\n'
1645         kept = []
1646         added = []
1647         deleted = []
1648         def diff_add_delete(fname, add, revision):
1649             diff = []
1650             diff.append(diff_hdr % fname)
1651             tmpfile = None
1652             origname = fname
1653             if add:
1654                 diff.append('--- %s\t(revision 0)\n' % fname)
1655                 rev = 'revision 0'
1656                 if revision and not fname in self.to_be_added:
1657                     rev = 'working copy'
1658                 diff.append('+++ %s\t(%s)\n' % (fname, rev))
1659                 fname = os.path.join(self.absdir, fname)
1660             else:
1661                 diff.append('--- %s\t(revision %s)\n' % (fname, revision or self.rev))
1662                 diff.append('+++ %s\t(working copy)\n' % fname)
1663                 fname = os.path.join(self.storedir, fname)
1664                
1665             try:
1666                 if revision is not None and not add:
1667                     (fd, tmpfile) = tempfile.mkstemp(prefix='osc_diff')
1668                     get_source_file(self.apiurl, self.prjname, self.name, origname, tmpfile, revision)
1669                     fname = tmpfile
1670                 if binary_file(fname):
1671                     what = 'added'
1672                     if not add:
1673                         what = 'deleted'
1674                     diff = diff[:1]
1675                     diff.append('Binary file \'%s\' %s.\n' % (origname, what))
1676                     return diff
1677                 tmpl = '+%s'
1678                 ltmpl = '@@ -0,0 +1,%d @@\n'
1679                 if not add:
1680                     tmpl = '-%s'
1681                     ltmpl = '@@ -1,%d +0,0 @@\n'
1682                 lines = [tmpl % i for i in open(fname, 'r').readlines()]
1683                 if len(lines):
1684                     diff.append(ltmpl % len(lines))
1685                     if not lines[-1].endswith('\n'):
1686                         lines.append('\n\\ No newline at end of file\n')
1687                 diff.extend(lines)
1688             finally:
1689                 if tmpfile is not None:
1690                     os.close(fd)
1691                     os.unlink(tmpfile)
1692             return diff
1693
1694         if revision is None:
1695             todo = self.todo or [i for i in self.filenamelist if not i in self.to_be_added]+self.to_be_added
1696             for fname in todo:
1697                 if fname in self.to_be_added and self.status(fname) == 'A':
1698                     added.append(fname)
1699                 elif fname in self.to_be_deleted:
1700                     deleted.append(fname)
1701                 elif fname in self.filenamelist:
1702                     kept.append(self.findfilebyname(fname))
1703                 elif fname in self.to_be_added and self.status(fname) == '!':
1704                     raise oscerr.OscIOError(None, 'file \'%s\' is marked as \'A\' but does not exist\n'\
1705                         '(either add the missing file or revert it)' % fname)
1706                 elif not ignoreUnversioned:
1707                     raise oscerr.OscIOError(None, 'file \'%s\' is not under version control' % fname)
1708         else:
1709             fm = self.get_files_meta(revision=revision)
1710             root = ET.fromstring(fm)
1711             rfiles = self.__get_files(root)
1712             # swap added and deleted
1713             kept, deleted, added, services = self.__get_rev_changes(rfiles)
1714             added = [f.name for f in added]
1715             added.extend([f for f in self.to_be_added if not f in kept])
1716             deleted = [f.name for f in deleted]
1717             deleted.extend(self.to_be_deleted)
1718             for f in added[:]:
1719                 if f in deleted:
1720                     added.remove(f)
1721                     deleted.remove(f)
1722 #        print kept, added, deleted
1723         for f in kept:
1724             state = self.status(f.name)
1725             if state in ('S', '?', '!'):
1726                 continue
1727             elif state == ' ' and revision is None:
1728                 continue
1729             elif revision and self.findfilebyname(f.name).md5 == f.md5 and state != 'M':
1730                 continue
1731             yield [diff_hdr % f.name]
1732             if revision is None:
1733                 yield get_source_file_diff(self.absdir, f.name, self.rev)
1734             else:
1735                 tmpfile = None
1736                 diff = []
1737                 try:
1738                     (fd, tmpfile) = tempfile.mkstemp(prefix='osc_diff')
1739                     get_source_file(self.apiurl, self.prjname, self.name, f.name, tmpfile, revision)
1740                     diff = get_source_file_diff(self.absdir, f.name, revision,
1741                         os.path.basename(tmpfile), os.path.dirname(tmpfile), f.name)
1742                 finally:
1743                     if tmpfile is not None:
1744                         os.close(fd)
1745                         os.unlink(tmpfile)
1746                 yield diff
1747
1748         for f in added:
1749             yield diff_add_delete(f, True, revision)
1750         for f in deleted:
1751             yield diff_add_delete(f, False, revision)
1752
1753     def merge(self, otherpac):
1754         self.todo += otherpac.todo
1755
1756     def __str__(self):
1757         r = """
1758 name: %s
1759 prjname: %s
1760 workingdir: %s
1761 localfilelist: %s
1762 linkinfo: %s
1763 rev: %s
1764 'todo' files: %s
1765 """ % (self.name,
1766         self.prjname,
1767         self.dir,
1768         '\n               '.join(self.filenamelist),
1769         self.linkinfo,
1770         self.rev,
1771         self.todo)
1772
1773         return r
1774
1775
1776     def read_meta_from_spec(self, spec = None):
1777         import glob
1778         if spec:
1779             specfile = spec
1780         else:
1781             # scan for spec files
1782             speclist = glob.glob(os.path.join(self.dir, '*.spec'))
1783             if len(speclist) == 1:
1784                 specfile = speclist[0]
1785             elif len(speclist) > 1:
1786                 print 'the following specfiles were found:'
1787                 for filename in speclist:
1788                     print filename
1789                 print 'please specify one with --specfile'
1790                 sys.exit(1)
1791             else:
1792                 print 'no specfile was found - please specify one ' \
1793                       'with --specfile'
1794                 sys.exit(1)
1795
1796         data = read_meta_from_spec(specfile, 'Summary', 'Url', '%description')
1797         self.summary = data.get('Summary', '')
1798         self.url = data.get('Url', '')
1799         self.descr = data.get('%description', '')
1800
1801
1802     def update_package_meta(self, force=False):
1803         """
1804         for the updatepacmetafromspec subcommand
1805             argument force supress the confirm question
1806         """
1807
1808         m = ''.join(show_package_meta(self.apiurl, self.prjname, self.name))
1809
1810         root = ET.fromstring(m)
1811         root.find('title').text = self.summary
1812         root.find('description').text = ''.join(self.descr)
1813         url = root.find('url')
1814         if url == None:
1815             url = ET.SubElement(root, 'url')
1816         url.text = self.url
1817
1818         u = makeurl(self.apiurl, ['source', self.prjname, self.name, '_meta'])
1819         mf = metafile(u, ET.tostring(root))
1820
1821         if not force:
1822             print '*' * 36, 'old', '*' * 36
1823             print m
1824             print '*' * 36, 'new', '*' * 36
1825             print ET.tostring(root)
1826             print '*' * 72
1827             repl = raw_input('Write? (y/N/e) ')
1828         else:
1829             repl = 'y'
1830
1831         if repl == 'y':
1832             mf.sync()
1833         elif repl == 'e':
1834             mf.edit()
1835
1836         mf.discard()
1837
1838     def mark_frozen(self):
1839         store_write_string(self.absdir, '_frozenlink', '')
1840         print
1841         print "The link in this package is currently broken. Checking"
1842         print "out the last working version instead; please use 'osc pull'"
1843         print "to repair the link."
1844         print
1845
1846     def unmark_frozen(self):
1847         if os.path.exists(os.path.join(self.storedir, '_frozenlink')):
1848             os.unlink(os.path.join(self.storedir, '_frozenlink'))
1849
1850     def latest_rev(self, include_service_files=False):
1851         if self.islinkrepair():
1852             upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrepair=1, meta=self.meta, include_service_files=include_service_files)
1853         elif self.islink() and self.isexpanded():
1854             if self.isfrozen() or self.ispulled():
1855                 upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev=self.linkinfo.srcmd5, meta=self.meta, include_service_files=include_service_files)
1856             else:
1857                 try:
1858                     upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, meta=self.meta, include_service_files=include_service_files)
1859                 except:
1860                     try:
1861                         upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev=self.linkinfo.srcmd5, meta=self.meta, include_service_files=include_service_files)
1862                     except:
1863                         upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev="base", meta=self.meta, include_service_files=include_service_files)
1864                     self.mark_frozen()
1865         else:
1866             upstream_rev = show_upstream_rev(self.apiurl, self.prjname, self.name, meta=self.meta, include_service_files=include_service_files)
1867         return upstream_rev
1868
1869     def __get_files(self, fmeta_root):
1870         f = []
1871         if fmeta_root.get('rev') is None and len(fmeta_root.findall('entry')) > 0:
1872             raise oscerr.APIError('missing rev attribute in _files:\n%s' % ''.join(ET.tostring(fmeta_root)))
1873         for i in fmeta_root.findall('entry'):
1874             skipped = i.get('skipped') is not None
1875             f.append(File(i.get('name'), i.get('md5'),
1876                      int(i.get('size')), int(i.get('mtime')), skipped))
1877         return f
1878
1879     def __get_rev_changes(self, revfiles):
1880         kept = []
1881         added = []
1882         deleted = []
1883         services = []
1884         revfilenames = []
1885         for f in revfiles:
1886             revfilenames.append(f.name)
1887             # treat skipped like deleted files
1888             if f.skipped:
1889                 if f.name.startswith('_service:'):
1890                     services.append(f)
1891                 else:
1892                     deleted.append(f)
1893                 continue
1894             # treat skipped like added files
1895             # problem: this overwrites existing files during the update
1896             # (because skipped files aren't in self.filenamelist_unvers)
1897             if f.name in self.filenamelist and not f.name in self.skipped:
1898                 kept.append(f)
1899             else:
1900                 added.append(f)
1901         for f in self.filelist:
1902             if not f.name in revfilenames:
1903                 deleted.append(f)
1904
1905         return kept, added, deleted, services
1906
1907     def update(self, rev = None, service_files = False, size_limit = None):
1908         import tempfile
1909         rfiles = []
1910         # size_limit is only temporary for this update
1911         old_size_limit = self.size_limit
1912         if not size_limit is None:
1913             self.size_limit = int(size_limit)
1914         if os.path.isfile(os.path.join(self.storedir, '_in_update', '_files')):
1915             print 'resuming broken update...'
1916             root = ET.parse(os.path.join(self.storedir, '_in_update', '_files')).getroot()
1917             rfiles = self.__get_files(root)
1918             kept, added, deleted, services = self.__get_rev_changes(rfiles)
1919             # check if we aborted in the middle of a file update
1920             broken_file = os.listdir(os.path.join(self.storedir, '_in_update'))
1921             broken_file.remove('_files')
1922             if len(broken_file) == 1:
1923                 origfile = os.path.join(self.storedir, '_in_update', broken_file[0])
1924                 wcfile = os.path.join(self.absdir, broken_file[0])
1925                 origfile_md5 = dgst(origfile)
1926                 origfile_meta = self.findfilebyname(broken_file[0])
1927                 if origfile.endswith('.copy'):
1928                     # ok it seems we aborted at some point during the copy process
1929                     # (copy process == copy wcfile to the _in_update dir). remove file+continue
1930                     os.unlink(origfile)
1931                 elif self.findfilebyname(broken_file[0]) is None:
1932                     # should we remove this file from _in_update? if we don't
1933                     # the user has no chance to continue without removing the file manually
1934                     raise oscerr.PackageInternalError(self.prjname, self.name,
1935                         '\'%s\' is not known by meta but exists in \'_in_update\' dir')
1936                 elif os.path.isfile(wcfile) and dgst(wcfile) != origfile_md5:
1937                     (fd, tmpfile) = tempfile.mkstemp(dir=self.absdir, prefix=broken_file[0]+'.')
1938                     os.close(fd)
1939                     os.rename(wcfile, tmpfile)
1940                     os.rename(origfile, wcfile)
1941                     print 'warning: it seems you modified \'%s\' after the broken ' \
1942                           'update. Restored original file and saved modified version ' \
1943                           'to \'%s\'.' % (wcfile, tmpfile)
1944                 elif not os.path.isfile(wcfile):
1945                     # this is strange... because it existed before the update. restore it
1946                     os.rename(origfile, wcfile)
1947                 else:
1948                     # everything seems to be ok
1949                     os.unlink(origfile)
1950             elif len(broken_file) > 1:
1951                 raise oscerr.PackageInternalError(self.prjname, self.name, 'too many files in \'_in_update\' dir')
1952             tmp = rfiles[:]
1953             for f in tmp:
1954                 if os.path.exists(os.path.join(self.storedir, f.name)):
1955                     if dgst(os.path.join(self.storedir, f.name)) == f.md5:
1956                         if f in kept:
1957                             kept.remove(f)
1958                         elif f in added:
1959                             added.remove(f)
1960                         # this can't happen
1961                         elif f in deleted:
1962                             deleted.remove(f)
1963             if not service_files:
1964                 services = []
1965             self.__update(kept, added, deleted, services, ET.tostring(root), root.get('rev'))
1966             os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
1967             os.rmdir(os.path.join(self.storedir, '_in_update'))
1968         # ok everything is ok (hopefully)...
1969         fm = self.get_files_meta(revision=rev)
1970         root = ET.fromstring(fm)
1971         rfiles = self.__get_files(root)
1972         store_write_string(self.absdir, '_files', fm + '\n', subdir='_in_update')
1973         kept, added, deleted, services = self.__get_rev_changes(rfiles)
1974         if not service_files:
1975             services = []
1976         self.__update(kept, added, deleted, services, fm, root.get('rev'))
1977         os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
1978         if os.path.isdir(os.path.join(self.storedir, '_in_update')):
1979             os.rmdir(os.path.join(self.storedir, '_in_update'))
1980         self.size_limit = old_size_limit
1981
1982     def __update(self, kept, added, deleted, services, fm, rev):
1983         pathn = getTransActPath(self.dir)
1984         # check for conflicts with existing files
1985         for f in added:
1986             if f.name in self.filenamelist_unvers:
1987                 raise oscerr.PackageFileConflict(self.prjname, self.name, f.name,
1988                     'failed to add file \'%s\' file/dir with the same name already exists' % f.name)
1989         # ok, the update can't fail due to existing files
1990         for f in added:
1991             self.updatefile(f.name, rev, f.mtime)
1992             print statfrmt('A', os.path.join(pathn, f.name))
1993         for f in deleted:
1994             # if the storefile doesn't exist we're resuming an aborted update:
1995             # the file was already deleted but we cannot know this
1996             # OR we're processing a _service: file (simply keep the file)
1997             if os.path.isfile(os.path.join(self.storedir, f.name)) and self.status(f.name) != 'M':
1998 #            if self.status(f.name) != 'M':
1999                 self.delete_localfile(f.name)
2000             self.delete_storefile(f.name)
2001             print statfrmt('D', os.path.join(pathn, f.name))
2002             if f.name in self.to_be_deleted:
2003                 self.to_be_deleted.remove(f.name)
2004                 self.write_deletelist()
2005
2006         for f in kept:
2007             state = self.status(f.name)
2008 #            print f.name, state
2009             if state == 'M' and self.findfilebyname(f.name).md5 == f.md5:
2010                 # remote file didn't change
2011                 pass
2012             elif state == 'M':
2013                 # try to merge changes
2014                 merge_status = self.mergefile(f.name, rev, f.mtime)
2015                 print statfrmt(merge_status, os.path.join(pathn, f.name))
2016             elif state == '!':
2017                 self.updatefile(f.name, rev, f.mtime)
2018                 print 'Restored \'%s\'' % os.path.join(pathn, f.name)
2019             elif state == 'C':
2020                 get_source_file(self.apiurl, self.prjname, self.name, f.name,
2021                     targetfilename=os.path.join(self.storedir, f.name), revision=rev,
2022                     progress_obj=self.progress_obj, mtime=f.mtime, meta=self.meta)
2023                 print 'skipping \'%s\' (this is due to conflicts)' % f.name
2024             elif state == 'D' and self.findfilebyname(f.name).md5 != f.md5:
2025                 # XXX: in the worst case we might end up with f.name being
2026                 # in _to_be_deleted and in _in_conflict... this needs to be checked
2027                 if os.path.exists(os.path.join(self.absdir, f.name)):
2028                     merge_status = self.mergefile(f.name, rev, f.mtime)
2029                     print statfrmt(merge_status, os.path.join(pathn, f.name))
2030                     if merge_status == 'C':
2031                         # state changes from delete to conflict
2032                         self.to_be_deleted.remove(f.name)
2033                         self.write_deletelist()
2034                 else:
2035                     # XXX: we cannot recover this case because we've no file
2036                     # to backup
2037                     self.updatefile(f.name, rev, f.mtime)
2038                     print statfrmt('U', os.path.join(pathn, f.name))
2039             elif state == ' ' and self.findfilebyname(f.name).md5 != f.md5:
2040                 self.updatefile(f.name, rev, f.mtime)
2041                 print statfrmt('U', os.path.join(pathn, f.name))
2042
2043         # checkout service files
2044         for f in services:
2045             get_source_file(self.apiurl, self.prjname, self.name, f.name,
2046                 targetfilename=os.path.join(self.absdir, f.name), revision=rev,
2047                 progress_obj=self.progress_obj, mtime=f.mtime, meta=self.meta)
2048             print statfrmt('A', os.path.join(pathn, f.name))
2049         store_write_string(self.absdir, '_files', fm + '\n')
2050         if not self.meta:
2051             self.update_local_pacmeta()
2052         self.update_datastructs()
2053
2054         print 'At revision %s.' % self.rev
2055
2056     def run_source_services(self, mode=None, singleservice=None, verbose=None):
2057         if self.name.startswith("_"):
2058             return 0
2059         curdir = os.getcwd()
2060         os.chdir(self.absdir) # e.g. /usr/lib/obs/service/verify_file fails if not inside the project dir.
2061         si = Serviceinfo()
2062         if self.filenamelist.count('_service') or self.filenamelist_unvers.count('_service'):
2063             service = ET.parse(os.path.join(self.absdir, '_service')).getroot()
2064             si.read(service)
2065         si.getProjectGlobalServices(self.apiurl, self.prjname, self.name)
2066         r = si.execute(self.absdir, mode, singleservice, verbose)
2067         os.chdir(curdir)
2068         return r
2069
2070     def prepare_filelist(self):
2071         """Prepare a list of files, which will be processed by process_filelist
2072         method. This allows easy modifications of a file list in commit
2073         phase.
2074         """
2075         if not self.todo:
2076             self.todo = self.filenamelist + self.filenamelist_unvers
2077         self.todo.sort()
2078
2079         ret = ""
2080         for f in [f for f in self.todo if not os.path.isdir(f)]:
2081             action = 'leave'
2082             status = self.status(f)
2083             if status == 'S':
2084                 continue
2085             if status == '!':
2086                 action = 'remove'
2087             ret += "%s %s %s\n" % (action, status, f)
2088
2089         ret += """
2090 # Edit a filelist for package \'%s\'
2091 # Commands:
2092 # l, leave = leave a file as is
2093 # r, remove = remove a file
2094 # a, add   = add a file
2095 #
2096 # If you remove file from a list, it will be unchanged
2097 # If you remove all, commit will be aborted""" % self.name
2098
2099         return ret
2100
2101     def edit_filelist(self):
2102         """Opens a package list in editor for editing. This allows easy
2103         modifications of it just by simple text editing
2104         """
2105
2106         import tempfile
2107         (fd, filename) = tempfile.mkstemp(prefix = 'osc-filelist', suffix = '.txt')
2108         f = os.fdopen(fd, 'w')
2109         f.write(self.prepare_filelist())
2110         f.close()
2111         mtime_orig = os.stat(filename).st_mtime
2112
2113         while 1:
2114             run_editor(filename)
2115             mtime = os.stat(filename).st_mtime
2116             if mtime_orig < mtime:
2117                 filelist = open(filename).readlines()
2118                 os.unlink(filename)
2119                 break
2120             else:
2121                 raise oscerr.UserAbort()
2122
2123         return self.process_filelist(filelist)
2124
2125     def process_filelist(self, filelist):
2126         """Process a filelist - it add/remove or leave files. This depends on
2127         user input. If no file is processed, it raises an ValueError
2128         """
2129
2130         loop = False
2131         for line in [l.strip() for l in filelist if (l[0] != "#" or l.strip() != '')]:
2132
2133             foo = line.split(' ')
2134             if len(foo) == 4:
2135                 action, state, name = (foo[0], ' ', foo[3])
2136             elif len(foo) == 3:
2137                 action, state, name = (foo[0], foo[1], foo[2])
2138             else:
2139                 break
2140             action = action.lower()
2141             loop = True
2142
2143             if action in ('r', 'remove'):
2144                 if self.status(name) == '?':
2145                     os.unlink(name)
2146                     if name in self.todo:
2147                         self.todo.remove(name)
2148                 else:
2149                     self.delete_file(name, True)
2150             elif action in ('a', 'add'):
2151                 if self.status(name) != '?':
2152                     print "Cannot add file %s with state %s, skipped" % (name, self.status(name))
2153                 else:
2154                     self.addfile(name)
2155             elif action in ('l', 'leave'):
2156                 pass
2157             else:
2158                 raise ValueError("Unknow action `%s'" % action)
2159
2160         if not loop:
2161             raise ValueError("Empty filelist")
2162
2163     def revert(self, filename):
2164         if not filename in self.filenamelist and not filename in self.to_be_added:
2165             raise oscerr.OscIOError(None, 'file \'%s\' is not under version control' % filename)
2166         elif filename in self.skipped:
2167             raise oscerr.OscIOError(None, 'file \'%s\' is marked as skipped and cannot be reverted' % filename)
2168         if filename in self.filenamelist and not os.path.exists(os.path.join(self.storedir, filename)):
2169             raise oscerr.PackageInternalError('file \'%s\' is listed in filenamelist but no storefile exists' % filename)
2170         state = self.status(filename)
2171         if not (state == 'A' or state == '!' and filename in self.to_be_added):
2172             shutil.copyfile(os.path.join(self.storedir, filename), os.path.join(self.absdir, filename))
2173         if state == 'D':
2174             self.to_be_deleted.remove(filename)
2175             self.write_deletelist()
2176         elif state == 'C':
2177             self.clear_from_conflictlist(filename)
2178         elif state in ('A', 'R') or state == '!' and filename in self.to_be_added:
2179             self.to_be_added.remove(filename)
2180             self.write_addlist()
2181
2182     @staticmethod
2183     def init_package(apiurl, project, package, dir, size_limit=None, meta=False, progress_obj=None):
2184         global store
2185
2186         if not os.path.exists(dir):
2187             os.mkdir(dir)
2188         elif not os.path.isdir(dir):
2189             raise oscerr.OscIOError(None, 'error: \'%s\' is no directory' % dir)
2190         if os.path.exists(os.path.join(dir, store)):
2191             raise oscerr.OscIOError(None, 'error: \'%s\' is already an initialized osc working copy' % dir)
2192         else:
2193             os.mkdir(os.path.join(dir, store))
2194         store_write_project(dir, project)
2195         store_write_string(dir, '_package', package + '\n')
2196         store_write_apiurl(dir, apiurl)
2197         if meta:
2198             store_write_string(dir, '_meta_mode', '')
2199         if size_limit:
2200             store_write_string(dir, '_size_limit', str(size_limit) + '\n')
2201         store_write_string(dir, '_files', '<directory />' + '\n')
2202         store_write_string(dir, '_osclib_version', __store_version__ + '\n')
2203         return Package(dir, progress_obj=progress_obj, size_limit=size_limit)
2204
2205
2206 class AbstractState:
2207     """
2208     Base class which represents state-like objects (<review />, <state />).
2209     """
2210     def __init__(self, tag):
2211         self.__tag = tag
2212
2213     def get_node_attrs(self):
2214         """return attributes for the tag/element"""
2215         raise NotImplementedError()
2216
2217     def get_node_name(self):
2218         """return tag/element name"""
2219         return self.__tag
2220
2221     def get_comment(self):
2222         """return data from <comment /> tag"""
2223         raise NotImplementedError()
2224
2225     def to_xml(self):
2226         """serialize object to XML"""
2227         root = ET.Element(self.get_node_name())
2228         for attr in self.get_node_attrs():
2229             val = getattr(self, attr)
2230             if not val is None:
2231                 root.set(attr, val)
2232         if self.get_comment():
2233             ET.SubElement(root, 'comment').text = self.get_comment()
2234         return root
2235
2236     def to_str(self):
2237         """return "pretty" XML data"""
2238         root = self.to_xml()
2239         xmlindent(root)
2240         return ET.tostring(root)
2241
2242
2243 class ReviewState(AbstractState):
2244     """Represents the review state in a request"""
2245     def __init__(self, review_node):
2246         if not review_node.get('state'):
2247             raise oscerr.APIError('invalid review node (state attr expected): %s' % \
2248                 ET.tostring(review_node))
2249         AbstractState.__init__(self, review_node.tag)
2250         self.state = review_node.get('state')
2251         self.by_user = review_node.get('by_user')
2252         self.by_group = review_node.get('by_group')
2253         self.by_project = review_node.get('by_project')
2254         self.by_package = review_node.get('by_package')
2255         self.who = review_node.get('who')
2256         self.when = review_node.get('when')
2257         self.comment = ''
2258         if not review_node.find('comment') is None and \
2259             review_node.find('comment').text:
2260             self.comment = review_node.find('comment').text.strip()
2261
2262     def get_node_attrs(self):
2263         return ('state', 'by_user', 'by_group', 'by_project', 'by_package', 'who', 'when')
2264
2265     def get_comment(self):
2266         return self.comment
2267
2268
2269 class RequestState(AbstractState):
2270     """Represents the state of a request"""
2271     def __init__(self, state_node):
2272         if not state_node.get('name'):
2273             raise oscerr.APIError('invalid request state node (name attr expected): %s' % \
2274                 ET.tostring(state_node))
2275         AbstractState.__init__(self, state_node.tag)
2276         self.name = state_node.get('name')
2277         self.who = state_node.get('who')
2278         self.when = state_node.get('when')
2279         self.comment = ''
2280         if not state_node.find('comment') is None and \
2281             state_node.find('comment').text:
2282             self.comment = state_node.find('comment').text.strip()
2283
2284     def get_node_attrs(self):
2285         return ('name', 'who', 'when')
2286
2287     def get_comment(self):
2288         return self.comment
2289
2290
2291 class Action:
2292     """
2293     Represents a <action /> element of a Request.
2294     This class is quite common so that it can be used for all different
2295     action types. Note: instances only provide attributes for their specific
2296     type.
2297     Examples:
2298       r = Action('set_bugowner', tgt_project='foo', person_name='buguser')
2299       # available attributes: r.type (== 'set_bugowner'), r.tgt_project (== 'foo'), r.tgt_package (== None)
2300       r.to_str() ->
2301       <action type="set_bugowner">
2302         <target project="foo" />
2303         <person name="buguser" />
2304       </action>
2305       ##
2306       r = Action('delete', tgt_project='foo', tgt_package='bar')
2307       # available attributes: r.type (== 'delete'), r.tgt_project (== 'foo'), r.tgt_package (=='bar')
2308       r.to_str() ->
2309       <action type="delete">
2310         <target package="bar" project="foo" />
2311       </action>
2312     """
2313
2314     # allowed types + the corresponding (allowed) attributes
2315     type_args = {'submit': ('src_project', 'src_package', 'src_rev', 'tgt_project', 'tgt_package', 'opt_sourceupdate',
2316                             'acceptinfo_rev', 'acceptinfo_srcmd5', 'acceptinfo_xsrcmd5', 'acceptinfo_osrcmd5',
2317                             'acceptinfo_oxsrcmd5', 'opt_updatelink'),
2318         'add_role': ('tgt_project', 'tgt_package', 'person_name', 'person_role', 'group_name', 'group_role'),
2319         'set_bugowner': ('tgt_project', 'tgt_package', 'person_name'), # obsoleted by add_role
2320         'maintenance_release': ('src_project', 'src_package', 'src_rev', 'tgt_project', 'tgt_package', 'person_name'),
2321         'maintenance_incident': ('src_project', 'tgt_project', 'person_name'),
2322         'delete': ('tgt_project', 'tgt_package'),
2323         'change_devel': ('src_project', 'src_package', 'tgt_project', 'tgt_package')}
2324     # attribute prefix to element name map (only needed for abbreviated attributes)
2325     prefix_to_elm = {'src': 'source', 'tgt': 'target', 'opt': 'options'}
2326
2327     def __init__(self, type, **kwargs):
2328         if not type in Action.type_args.keys():
2329             raise oscerr.WrongArgs('invalid action type: \'%s\'' % type)
2330         self.type = type
2331         for i in kwargs.keys():
2332             if not i in Action.type_args[type]:
2333                 raise oscerr.WrongArgs('invalid argument: \'%s\'' % i)
2334         # set all type specific attributes
2335         for i in Action.type_args[type]:
2336             if kwargs.has_key(i):
2337                 setattr(self, i, kwargs[i])
2338             else:
2339                 setattr(self, i, None)
2340
2341     def to_xml(self):
2342         """
2343         Serialize object to XML.
2344         The xml tag names and attributes are constructed from the instance's attributes.
2345         Example:
2346           self.group_name  -> tag name is "group", attribute name is "name"
2347           self.src_project -> tag name is "source" (translated via prefix_to_elm dict),
2348                               attribute name is "project"
2349         Attributes prefixed with "opt_" need a special handling, the resulting xml should
2350         look like this: opt_updatelink -> <options><updatelink>value</updatelink></options>.
2351         Attributes which are "None" will be skipped.
2352         """
2353         root = ET.Element('action', type=self.type)
2354         for i in Action.type_args[self.type]:
2355             prefix, attr = i.split('_', 1)
2356             val = getattr(self, i)
2357             if val is None:
2358                 continue
2359             elm = root.find(Action.prefix_to_elm.get(prefix, prefix))
2360             if elm is None:
2361                 elm = ET.Element(Action.prefix_to_elm.get(prefix, prefix))
2362                 root.append(elm)
2363             if prefix == 'opt':
2364                 ET.SubElement(elm, attr).text = val
2365             else:
2366                 elm.set(attr, val)
2367         return root
2368
2369     def to_str(self):
2370         """return "pretty" XML data"""
2371         root = self.to_xml()
2372         xmlindent(root)
2373         return ET.tostring(root)
2374
2375     @staticmethod
2376     def from_xml(action_node):
2377         """create action from XML"""
2378         if action_node is None or \
2379             not action_node.get('type') in Action.type_args.keys() or \
2380             not action_node.tag in ('action', 'submit'):
2381             raise oscerr.WrongArgs('invalid argument')
2382         elm_to_prefix = dict([(i[1], i[0]) for i in Action.prefix_to_elm.items()])
2383         kwargs = {}
2384         for node in action_node:
2385             prefix = elm_to_prefix.get(node.tag, node.tag)
2386             if prefix == 'opt':
2387                 data = [('opt_%s' % opt.tag, opt.text.strip()) for opt in node if opt.text]
2388             else:
2389                 data = [('%s_%s' % (prefix, k), v) for k, v in node.items()]
2390             kwargs.update(dict(data))
2391         return Action(action_node.get('type'), **kwargs)
2392
2393
2394 class Request:
2395     """Represents a request (<request />)"""
2396
2397     def __init__(self):
2398         self._init_attributes()
2399
2400     def _init_attributes(self):
2401         """initialize attributes with default values"""
2402         self.reqid = None
2403         self.title = ''
2404         self.description = ''
2405         self.state = None
2406         self.actions = []
2407         self.statehistory = []
2408         self.reviews = []
2409
2410     def read(self, root):
2411         """read in a request"""
2412         self._init_attributes()
2413         if not root.get('id'):
2414             raise oscerr.APIError('invalid request: %s\n' % ET.tostring(root))
2415         self.reqid = root.get('id')
2416         if root.find('state') is None:
2417             raise oscerr.APIError('invalid request (state expected): %s\n' % ET.tostring(root))
2418         self.state = RequestState(root.find('state'))
2419         action_nodes = root.findall('action')
2420         if not action_nodes:
2421             # check for old-style requests
2422             for i in root.findall('submit'):
2423                 i.set('type', 'submit')
2424                 action_nodes.append(i)
2425         for action in action_nodes:
2426             self.actions.append(Action.from_xml(action))
2427         for review in root.findall('review'):
2428             self.reviews.append(ReviewState(review))
2429         for hist_state in root.findall('history'):
2430             self.statehistory.append(RequestState(hist_state))
2431         if not root.find('title') is None:
2432             self.title = root.find('title').text.strip()
2433         if not root.find('description') is None and root.find('description').text:
2434             self.description = root.find('description').text.strip()
2435
2436     def add_action(self, type, **kwargs):
2437         """add a new action to the request"""
2438         self.actions.append(Action(type, **kwargs))
2439
2440     def get_actions(self, *types):
2441         """
2442         get all actions with a specific type
2443         (if types is empty return all actions)
2444         """
2445         if not types:
2446             return self.actions
2447         return [i for i in self.actions if i.type in types]
2448
2449     def get_creator(self):
2450         """return the creator of the request"""
2451         if len(self.statehistory):
2452             return self.statehistory[0].who
2453         return self.state.who
2454
2455     def to_xml(self):
2456         """serialize object to XML"""
2457         root = ET.Element('request')
2458         if not self.reqid is None:
2459             root.set('id', self.reqid)
2460         for action in self.actions:
2461             root.append(action.to_xml())
2462         if not self.state is None:
2463             root.append(self.state.to_xml())
2464         for review in self.reviews:
2465             root.append(review.to_xml())
2466         for hist in self.statehistory:
2467             root.append(hist.to_xml())
2468         if self.title:
2469             ET.SubElement(root, 'title').text = self.title
2470         if self.description:
2471             ET.SubElement(root, 'description').text = self.description
2472         return root
2473
2474     def to_str(self):
2475         """return "pretty" XML data"""
2476         root = self.to_xml()
2477         xmlindent(root)
2478         return ET.tostring(root)
2479
2480     @staticmethod
2481     def format_review(review, show_srcupdate=False):
2482         """
2483         format a review depending on the reviewer's type.
2484         A dict which contains the formatted str's is returned.
2485         """
2486
2487         d = {'state': '%s:' % review.state}
2488         if review.by_package:
2489            d['by'] = '%s/%s' % (review.by_project, review.by_package)
2490            d['type'] = 'Package'
2491         elif review.by_project:
2492            d['by'] = '%s' % review.by_project
2493            d['type'] = 'Project'
2494         elif review.by_group:
2495            d['by'] = '%s' % review.by_group
2496            d['type'] = 'Group'
2497         else:
2498            d['by'] = '%s' % review.by_user
2499            d['type'] = 'User'
2500         if review.who:
2501            d['by'] += '(%s)' % review.who
2502         return d
2503
2504     @staticmethod
2505     def format_action(action, show_srcupdate=False):
2506         """
2507         format an action depending on the action's type.
2508         A dict which contains the formatted str's is returned.
2509         """
2510         def prj_pkg_join(prj, pkg):
2511             if not pkg:
2512                 return prj or ''
2513             return '%s/%s' % (prj, pkg)
2514
2515         d = {'type': '%s:' % action.type}
2516         if action.type == 'set_bugowner':
2517             d['source'] = action.person_name
2518             d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2519         elif action.type == 'change_devel':
2520             d['source'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2521             d['target'] = 'developed in %s' % prj_pkg_join(action.src_project, action.src_package)
2522         elif action.type == 'maintenance_incident':
2523             d['source'] = '%s ->' % action.src_project
2524             d['target'] = action.tgt_project
2525         elif action.type == 'maintenance_release':
2526             d['source'] = '%s ->' % prj_pkg_join(action.src_project, action.src_package)
2527             d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2528         elif action.type == 'submit':
2529             srcupdate = ' '
2530             if action.opt_sourceupdate and show_srcupdate:
2531                 srcupdate = '(%s)' % action.opt_sourceupdate
2532             d['source'] = '%s%s ->' % (prj_pkg_join(action.src_project, action.src_package), srcupdate)
2533             tgt_package = action.tgt_package
2534             if action.src_package == action.tgt_package:
2535                 tgt_package = ''
2536             d['target'] = prj_pkg_join(action.tgt_project, tgt_package)
2537         elif action.type == 'add_role':
2538             roles = []
2539             if action.person_name and action.person_role:
2540                 roles.append('person: %s as %s' % (action.person_name, action.person_role))
2541             if action.group_name and action.group_role:
2542                 roles.append('group: %s as %s' % (action.group_name, action.group_role))
2543             d['source'] = ', '.join(roles)
2544             d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2545         elif action.type == 'delete':
2546             d['source'] = ''
2547             d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
2548         return d
2549
2550     def list_view(self):
2551         """return "list view" format"""
2552         import textwrap
2553         lines = ['%6s  State:%-10s By:%-12s When:%-19s' % (self.reqid, self.state.name, self.state.who, self.state.when)]
2554         tmpl = '        %(type)-16s %(source)-50s %(target)s'
2555         for action in self.actions:
2556             lines.append(tmpl % Request.format_action(action))
2557         tmpl = '        Review by %(type)-10s is %(state)-10s %(by)-50s'
2558         for review in self.reviews:
2559             lines.append(tmpl % Request.format_review(review))
2560         history = ['%s(%s)' % (hist.name, hist.who) for hist in self.statehistory]
2561         if history:
2562             lines.append('        From: %s' % ' -> '.join(history))
2563         if self.description:
2564             lines.append(textwrap.fill(self.description, width=80, initial_indent='        Descr: ',
2565                 subsequent_indent='               '))
2566         return '\n'.join(lines)
2567
2568     def __str__(self):
2569         """return "detailed" format"""
2570         lines = ['Request: #%s\n' % self.reqid]
2571         for action in self.actions:
2572             tmpl = '  %(type)-13s %(source)s %(target)s'
2573             if action.type == 'delete':
2574                 # remove 1 whitespace because source is empty
2575                 tmpl = '  %(type)-12s %(source)s %(target)s'
2576             lines.append(tmpl % Request.format_action(action, show_srcupdate=True))
2577         lines.append('\n\nMessage:')
2578         if self.description:
2579             lines.append(self.description)
2580         else:
2581             lines.append('<no message>')
2582         if self.state:
2583             lines.append('\nState:   %-10s %-12s %s' % (self.state.name, self.state.when, self.state.who))
2584             lines.append('Comment: %s' % (self.state.comment or '<no comment>'))
2585
2586         indent = '\n         '
2587         tmpl = '%(state)-10s %(by)-50s %(when)-12s %(who)-20s  %(comment)s'
2588         reviews = []
2589         for review in reversed(self.reviews):
2590             d = {'state': review.state}
2591             if review.by_user:
2592               d['by'] = "User: " + review.by_user
2593             if review.by_group:
2594               d['by'] = "Group: " + review.by_group
2595             if review.by_package:
2596               d['by'] = "Package: " + review.by_project + "/" + review.by_package 
2597             elif review.by_project:
2598               d['by'] = "Project: " + review.by_project
2599             d['when'] = review.when or ''
2600             d['who'] = review.who or ''
2601             d['comment'] = review.comment or ''
2602             reviews.append(tmpl % d)
2603         if reviews:
2604             lines.append('\nReview:  %s' % indent.join(reviews))
2605
2606         tmpl = '%(name)-10s %(when)-12s %(who)s'
2607         histories = []
2608         for hist in reversed(self.statehistory):
2609             d = {'name': hist.name, 'when': hist.when,
2610                 'who': hist.who}
2611             histories.append(tmpl % d)
2612         if histories:
2613             lines.append('\nHistory: %s' % indent.join(histories))
2614
2615         return '\n'.join(lines)
2616
2617     def __cmp__(self, other):
2618         return cmp(int(self.reqid), int(other.reqid))
2619
2620     def create(self, apiurl):
2621         """create a new request"""
2622         u = makeurl(apiurl, ['request'], query='cmd=create')
2623         f = http_POST(u, data=self.to_str())
2624         root = ET.fromstring(f.read())
2625         self.read(root)
2626
2627 def shorttime(t):
2628     """format time as Apr 02 18:19
2629     or                Apr 02  2005
2630     depending on whether it is in the current year
2631     """
2632     import time
2633
2634     if time.localtime()[0] == time.localtime(t)[0]:
2635         # same year
2636         return time.strftime('%b %d %H:%M',time.localtime(t))
2637     else:
2638         return time.strftime('%b %d  %Y',time.localtime(t))
2639
2640
2641 def is_project_dir(d):
2642     global store
2643
2644     return os.path.exists(os.path.join(d, store, '_project')) and not \
2645            os.path.exists(os.path.join(d, store, '_package'))
2646
2647
2648 def is_package_dir(d):
2649     global store
2650
2651     return os.path.exists(os.path.join(d, store, '_project')) and \
2652            os.path.exists(os.path.join(d, store, '_package'))
2653
2654 def parse_disturl(disturl):
2655     """Parse a disturl, returns tuple (apiurl, project, source, repository,
2656     revision), else raises an oscerr.WrongArgs exception
2657     """
2658
2659     global DISTURL_RE
2660
2661     m = DISTURL_RE.match(disturl)
2662     if not m:
2663         raise oscerr.WrongArgs("`%s' does not look like disturl" % disturl)
2664
2665     apiurl = m.group('apiurl')
2666     if apiurl.split('.')[0] != 'api':
2667         apiurl = 'https://api.' + ".".join(apiurl.split('.')[1:])
2668     return (apiurl, m.group('project'), m.group('source'), m.group('repository'), m.group('revision'))
2669
2670 def parse_buildlogurl(buildlogurl):
2671     """Parse a build log url, returns a tuple (apiurl, project, package,
2672     repository, arch), else raises oscerr.WrongArgs exception"""
2673
2674     global BUILDLOGURL_RE
2675
2676     m = BUILDLOGURL_RE.match(buildlogurl)
2677     if not m:
2678         raise oscerr.WrongArgs('\'%s\' does not look like url with a build log' % buildlogurl)
2679
2680     return (m.group('apiurl'), m.group('project'), m.group('package'), m.group('repository'), m.group('arch'))
2681
2682 def slash_split(l):
2683     """Split command line arguments like 'foo/bar' into 'foo' 'bar'.
2684     This is handy to allow copy/paste a project/package combination in this form.
2685
2686     Trailing slashes are removed before the split, because the split would
2687     otherwise give an additional empty string.
2688     """
2689     r = []
2690     for i in l:
2691         i = i.rstrip('/')
2692         r += i.split('/')
2693     return r
2694
2695 def expand_proj_pack(args, idx=0, howmany=0):
2696     """looks for occurance of '.' at the position idx.
2697     If howmany is 2, both proj and pack are expanded together
2698     using the current directory, or none of them, if not possible.
2699     If howmany is 0, proj is expanded if possible, then, if there
2700     is no idx+1 element in args (or args[idx+1] == '.'), pack is also
2701     expanded, if possible.
2702     If howmany is 1, only proj is expanded if possible.
2703
2704     If args[idx] does not exists, an implicit '.' is assumed.
2705     if not enough elements up to idx exist, an error is raised.
2706
2707     See also parseargs(args), slash_split(args), findpacs(args)
2708     All these need unification, somehow.
2709     """
2710
2711     # print args,idx,howmany
2712
2713     if len(args) < idx:
2714         raise oscerr.WrongArgs('not enough argument, expected at least %d' % idx)
2715
2716     if len(args) == idx:
2717         args += '.'
2718     if args[idx+0] == '.':
2719         if howmany == 0 and len(args) > idx+1:
2720             if args[idx+1] == '.':
2721                 # we have two dots.
2722                 # remove one dot and make sure to expand both proj and pack
2723                 args.pop(idx+1)
2724                 howmany = 2
2725             else:
2726                 howmany = 1
2727         # print args,idx,howmany
2728
2729         args[idx+0] = store_read_project('.')
2730         if howmany == 0:
2731             try:
2732                 package = store_read_package('.')
2733                 args.insert(idx+1, package)
2734             except:
2735                 pass
2736         elif howmany == 2:
2737             package = store_read_package('.')
2738             args.insert(idx+1, package)
2739     return args
2740
2741
2742 def findpacs(files, progress_obj=None):
2743     """collect Package objects belonging to the given files
2744     and make sure each Package is returned only once"""
2745     pacs = []
2746     for f in files:
2747         p = filedir_to_pac(f, progress_obj)
2748         known = None
2749         for i in pacs:
2750             if i.name == p.name:
2751                 known = i
2752                 break
2753         if known:
2754             i.merge(p)
2755         else:
2756             pacs.append(p)
2757     return pacs
2758
2759
2760 def filedir_to_pac(f, progress_obj=None):
2761     """Takes a working copy path, or a path to a file inside a working copy,
2762     and returns a Package object instance
2763
2764     If the argument was a filename, add it onto the "todo" list of the Package """
2765
2766     if os.path.isdir(f):
2767         wd = f
2768         p = Package(wd, progress_obj=progress_obj)
2769     else:
2770         wd = os.path.dirname(f) or os.curdir
2771         p = Package(wd, progress_obj=progress_obj)
2772         p.todo = [ os.path.basename(f) ]
2773     return p
2774
2775
2776 def read_filemeta(dir):
2777     global store
2778
2779     msg = '\'%s\' is not a valid working copy.' % dir
2780     filesmeta = os.path.join(dir, store, '_files')
2781     if not is_package_dir(dir):
2782         raise oscerr.NoWorkingCopy(msg)
2783     if not os.path.isfile(filesmeta):
2784         raise oscerr.NoWorkingCopy('%s (%s does not exist)' % (msg, filesmeta))
2785
2786     try:
2787         r = ET.parse(filesmeta)
2788     except SyntaxError, e:
2789         raise oscerr.NoWorkingCopy('%s\nWhen parsing .osc/_files, the following error was encountered:\n%s' % (msg, e))
2790     return r
2791
2792 def store_readlist(dir, name):
2793     global store
2794
2795     r = []
2796     if os.path.exists(os.path.join(dir, store, name)):
2797         r = [line.strip() for line in open(os.path.join(dir, store, name), 'r')]
2798     return r
2799
2800 def read_tobeadded(dir):
2801     return store_readlist(dir, '_to_be_added')
2802
2803 def read_tobedeleted(dir):
2804     return store_readlist(dir, '_to_be_deleted')
2805
2806 def read_sizelimit(dir):
2807     global store
2808
2809     r = None
2810     fname = os.path.join(dir, store, '_size_limit')
2811
2812     if os.path.exists(fname):
2813         r = open(fname).readline().strip()
2814
2815     if r is None or not r.isdigit():
2816         return None
2817     return int(r)
2818
2819 def read_inconflict(dir):
2820     return store_readlist(dir, '_in_conflict')
2821
2822 def parseargs(list_of_args):
2823     """Convenience method osc's commandline argument parsing.
2824
2825     If called with an empty tuple (or list), return a list containing the current directory.
2826     Otherwise, return a list of the arguments."""
2827     if list_of_args:
2828         return list(list_of_args)
2829     else:
2830         return [os.curdir]
2831
2832
2833 def statfrmt(statusletter, filename):
2834     return '%s    %s' % (statusletter, filename)
2835
2836
2837 def pathjoin(a, *p):
2838     """Join two or more pathname components, inserting '/' as needed. Cut leading ./"""
2839     path = os.path.join(a, *p)
2840     if path.startswith('./'):
2841         path = path[2:]
2842     return path
2843
2844
2845 def makeurl(baseurl, l, query=[]):
2846     """Given a list of path compoments, construct a complete URL.
2847
2848     Optional parameters for a query string can be given as a list, as a
2849     dictionary, or as an already assembled string.
2850     In case of a dictionary, the parameters will be urlencoded by this
2851     function. In case of a list not -- this is to be backwards compatible.
2852     """
2853
2854     if conf.config['verbose'] > 1:
2855         print 'makeurl:', baseurl, l, query
2856
2857     if type(query) == type(list()):
2858         query = '&'.join(query)
2859     elif type(query) == type(dict()):
2860         query = urlencode(query)
2861
2862     scheme, netloc = urlsplit(baseurl)[0:2]
2863     return urlunsplit((scheme, netloc, '/'.join(l), query, ''))
2864
2865
2866 def http_request(method, url, headers={}, data=None, file=None, timeout=100):
2867     """wrapper around urllib2.urlopen for error handling,
2868     and to support additional (PUT, DELETE) methods"""
2869
2870     filefd = None
2871
2872     if conf.config['http_debug']:
2873         print >>sys.stderr, '\n\n--', method, url
2874
2875     if method == 'POST' and not file and not data:
2876         # adding data to an urllib2 request transforms it into a POST
2877         data = ''
2878
2879     req = urllib2.Request(url)
2880     api_host_options = {}
2881     if conf.is_known_apiurl(url):
2882         # ok no external request
2883         urllib2.install_opener(conf._build_opener(url))
2884         api_host_options = conf.get_apiurl_api_host_options(url)
2885         for header, value in api_host_options['http_headers']:
2886             req.add_header(header, value)
2887
2888     req.get_method = lambda: method
2889
2890     # POST requests are application/x-www-form-urlencoded per default
2891     # since we change the request into PUT, we also need to adjust the content type header
2892     if method == 'PUT' or (method == 'POST' and data):
2893         req.add_header('Content-Type', 'application/octet-stream')
2894
2895     if type(headers) == type({}):
2896         for i in headers.keys():
2897             print headers[i]
2898             req.add_header(i, headers[i])
2899
2900     if file and not data:
2901         size = os.path.getsize(file)
2902         if size < 1024*512:
2903             data = open(file, 'rb').read()
2904         else:
2905             import mmap
2906             filefd = open(file, 'rb')
2907             try:
2908                 if sys.platform[:3] != 'win':
2909                     data = mmap.mmap(filefd.fileno(), os.path.getsize(file), mmap.MAP_SHARED, mmap.PROT_READ)
2910                 else:
2911                     data = mmap.mmap(filefd.fileno(), os.path.getsize(file))
2912                 data = buffer(data)
2913             except EnvironmentError, e:
2914                 if e.errno == 19:
2915                     sys.exit('\n\n%s\nThe file \'%s\' could not be memory mapped. It is ' \
2916                              '\non a filesystem which does not support this.' % (e, file))
2917                 elif hasattr(e, 'winerror') and e.winerror == 5:
2918                     # falling back to the default io
2919                     data = open(file, 'rb').read()
2920                 else:
2921                     raise
2922
2923     if conf.config['debug']: print >>sys.stderr, method, url
2924
2925     old_timeout = socket.getdefaulttimeout()
2926     # XXX: dirty hack as timeout doesn't work with python-m2crypto
2927     if old_timeout != timeout and not api_host_options.get('sslcertck'):
2928         socket.setdefaulttimeout(timeout)
2929     try:
2930         fd = urllib2.urlopen(req, data=data)
2931     finally:
2932         if old_timeout != timeout and not api_host_options.get('sslcertck'):
2933             socket.setdefaulttimeout(old_timeout)
2934         if hasattr(conf.cookiejar, 'save'):
2935             conf.cookiejar.save(ignore_discard=True)
2936
2937     if filefd: filefd.close()
2938
2939     return fd
2940
2941
2942 def http_GET(*args, **kwargs):    return http_request('GET', *args, **kwargs)
2943 def http_POST(*args, **kwargs):   return http_request('POST', *args, **kwargs)
2944 def http_PUT(*args, **kwargs):    return http_request('PUT', *args, **kwargs)
2945 def http_DELETE(*args, **kwargs): return http_request('DELETE', *args, **kwargs)
2946
2947
2948 def check_store_version(dir):
2949     global store
2950
2951     versionfile = os.path.join(dir, store, '_osclib_version')
2952     try:
2953         v = open(versionfile).read().strip()
2954     except:
2955         v = ''
2956
2957     if v == '':
2958         msg = 'Error: "%s" is not an osc package working copy.' % os.path.abspath(dir)
2959         if os.path.exists(os.path.join(dir, '.svn')):
2960             msg = msg + '\nTry svn instead of osc.'
2961         raise oscerr.NoWorkingCopy(msg)
2962
2963     if v != __store_version__:
2964         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']:
2965             # version is fine, no migration needed
2966             f = open(versionfile, 'w')
2967             f.write(__store_version__ + '\n')
2968             f.close()
2969             return
2970         msg = 'The osc metadata of your working copy "%s"' % dir
2971         msg += '\nhas __store_version__ = %s, but it should be %s' % (v, __store_version__)
2972         msg += '\nPlease do a fresh checkout or update your client. Sorry about the inconvenience.'
2973         raise oscerr.WorkingCopyWrongVersion, msg
2974
2975
2976 def meta_get_packagelist(apiurl, prj, deleted=None):
2977
2978     query = {}
2979     if deleted:
2980        query['deleted'] = 1
2981
2982     u = makeurl(apiurl, ['source', prj], query)
2983     f = http_GET(u)
2984     root = ET.parse(f).getroot()
2985     return [ node.get('name') for node in root.findall('entry') ]
2986
2987
2988 def meta_get_filelist(apiurl, prj, package, verbose=False, expand=False, revision=None, meta=False):
2989     """return a list of file names,
2990     or a list File() instances if verbose=True"""
2991
2992     query = {}
2993     if expand:
2994         query['expand'] = 1
2995     if meta:
2996         query['meta'] = 1
2997     if revision:
2998         query['rev'] = revision
2999     else:
3000         query['rev'] = 'latest'
3001
3002     u = makeurl(apiurl, ['source', prj, package], query=query)
3003     f = http_GET(u)
3004     root = ET.parse(f).getroot()
3005
3006     if not verbose:
3007         return [ node.get('name') for node in root.findall('entry') ]
3008
3009     else:
3010         l = []
3011         # rev = int(root.get('rev'))    # don't force int. also allow srcmd5 here.
3012         rev = root.get('rev')
3013         for node in root.findall('entry'):
3014             f = File(node.get('name'),
3015                      node.get('md5'),
3016                      int(node.get('size')),
3017                      int(node.get('mtime')))
3018             f.rev = rev
3019             l.append(f)
3020         return l
3021
3022
3023 def meta_get_project_list(apiurl, deleted=None):
3024     query = {}
3025     if deleted:
3026         query['deleted'] = 1
3027
3028     u = makeurl(apiurl, ['source'], query)
3029     f = http_GET(u)
3030     root = ET.parse(f).getroot()
3031     return sorted([ node.get('name') for node in root if node.get('name')])
3032
3033
3034 def show_project_meta(apiurl, prj):
3035     url = makeurl(apiurl, ['source', prj, '_meta'])
3036     f = http_GET(url)
3037     return f.readlines()
3038
3039
3040 def show_project_conf(apiurl, prj):
3041     url = makeurl(apiurl, ['source', prj, '_config'])
3042     f = http_GET(url)
3043     return f.readlines()
3044
3045
3046 def show_package_trigger_reason(apiurl, prj, pac, repo, arch):
3047     url = makeurl(apiurl, ['build', prj, repo, arch, pac, '_reason'])
3048     try:
3049         f = http_GET(url)
3050         return f.read()
3051     except urllib2.HTTPError, e:
3052         e.osc_msg = 'Error getting trigger reason for project \'%s\' package \'%s\'' % (prj, pac)
3053         raise
3054
3055
3056 def show_package_meta(apiurl, prj, pac, meta=False):
3057     query = {}
3058     if meta:
3059         query['meta'] = 1
3060
3061     # packages like _pattern and _project do not have a _meta file
3062     if pac.startswith('_pattern') or pac.startswith('_project'):
3063         return ""
3064
3065     url = makeurl(apiurl, ['source', prj, pac, '_meta'], query)
3066     try:
3067         f = http_GET(url)
3068         return f.readlines()
3069     except urllib2.HTTPError, e:
3070         e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
3071         raise
3072
3073
3074 def show_attribute_meta(apiurl, prj, pac, subpac, attribute, with_defaults, with_project):
3075     path=[]
3076     path.append('source')
3077     path.append(prj)
3078     if pac:
3079         path.append(pac)
3080     if pac and subpac:
3081         path.append(subpac)
3082     path.append('_attribute')
3083     if attribute:
3084         path.append(attribute)
3085     query=[]
3086     if with_defaults:
3087         query.append("with_default=1")
3088     if with_project:
3089         query.append("with_project=1")
3090     url = makeurl(apiurl, path, query)
3091     try:
3092         f = http_GET(url)
3093         return f.readlines()
3094     except urllib2.HTTPError, e:
3095         e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
3096         raise
3097
3098
3099 def show_develproject(apiurl, prj, pac, xml_node=False):
3100     m = show_package_meta(apiurl, prj, pac)
3101     node = ET.fromstring(''.join(m)).find('devel')
3102     if not node is None:
3103         if xml_node:
3104             return node
3105         return node.get('project')
3106     return None
3107
3108
3109 def show_package_disabled_repos(apiurl, prj, pac):
3110     m = show_package_meta(apiurl, prj, pac)
3111     #FIXME: don't work if all repos of a project are disabled and only some are enabled since <disable/> is empty
3112     try:
3113         root = ET.fromstring(''.join(m))
3114         elm = root.find('build')
3115         r = [ node.get('repository') for node in elm.findall('disable')]
3116         return r
3117     except:
3118         return None
3119
3120
3121 def show_pattern_metalist(apiurl, prj):
3122     url = makeurl(apiurl, ['source', prj, '_pattern'])
3123     try:
3124         f = http_GET(url)
3125         tree = ET.parse(f)
3126     except urllib2.HTTPError, e:
3127         e.osc_msg = 'show_pattern_metalist: Error getting pattern list for project \'%s\'' % prj
3128         raise
3129     r = [ node.get('name') for node in tree.getroot() ]
3130     r.sort()
3131     return r
3132
3133
3134 def show_pattern_meta(apiurl, prj, pattern):
3135     url = makeurl(apiurl, ['source', prj, '_pattern', pattern])
3136     try:
3137         f = http_GET(url)
3138         return f.readlines()
3139     except urllib2.HTTPError, e:
3140         e.osc_msg = 'show_pattern_meta: Error getting pattern \'%s\' for project \'%s\'' % (pattern, prj)
3141         raise
3142
3143
3144 class metafile:
3145     """metafile that can be manipulated and is stored back after manipulation."""
3146     def __init__(self, url, input, change_is_required=False, file_ext='.xml'):
3147         import tempfile
3148
3149         self.url = url
3150         self.change_is_required = change_is_required
3151         (fd, self.filename) = tempfile.mkstemp(prefix = 'osc_metafile.', suffix = file_ext)
3152         f = os.fdopen(fd, 'w')
3153         f.write(''.join(input))
3154         f.close()
3155         self.hash_orig = dgst(self.filename)
3156
3157     def sync(self):
3158         if self.change_is_required and self.hash_orig == dgst(self.filename):
3159             print 'File unchanged. Not saving.'
3160             os.unlink(self.filename)
3161             return
3162
3163         print 'Sending meta data...'
3164         # don't do any exception handling... it's up to the caller what to do in case
3165         # of an exception
3166         http_PUT(self.url, file=self.filename)
3167         os.unlink(self.filename)
3168         print 'Done.'
3169
3170     def edit(self):
3171         try:
3172             while 1:
3173                 run_editor(self.filename)
3174                 try:
3175                     self.sync()
3176                     break
3177                 except urllib2.HTTPError, e:
3178                     error_help = "%d" % e.code
3179                     if e.headers.get('X-Opensuse-Errorcode'):
3180                         error_help = "%s (%d)" % (e.headers.get('X-Opensuse-Errorcode'), e.code)
3181
3182                     print >>sys.stderr, 'BuildService API error:', error_help
3183                     # examine the error - we can't raise an exception because we might want
3184                     # to try again
3185                     data = e.read()
3186                     if '<summary>' in data:
3187                         print >>sys.stderr, data.split('<summary>')[1].split('</summary>')[0]
3188                     ri = raw_input('Try again? ([y/N]): ')
3189                     if ri not in ['y', 'Y']:
3190                         break
3191         finally:
3192             self.discard()
3193
3194     def discard(self):
3195         if os.path.exists(self.filename):
3196             print 'discarding %s' % self.filename
3197             os.unlink(self.filename)
3198
3199
3200 # different types of metadata
3201 metatypes = { 'prj':     { 'path': 'source/%s/_meta',
3202                            'template': new_project_templ,
3203                            'file_ext': '.xml'
3204                          },
3205               'pkg':     { 'path'     : 'source/%s/%s/_meta',
3206                            'template': new_package_templ,
3207                            'file_ext': '.xml'
3208                          },
3209               'attribute':     { 'path'     : 'source/%s/%s/_meta',
3210                            'template': new_attribute_templ,
3211                            'file_ext': '.xml'
3212                          },
3213               'prjconf': { 'path': 'source/%s/_config',
3214                            'template': '',
3215                            'file_ext': '.txt'
3216                          },
3217               'user':    { 'path': 'person/%s',
3218                            'template': new_user_template,
3219                            'file_ext': '.xml'
3220                          },
3221               'pattern': { 'path': 'source/%s/_pattern/%s',
3222                            'template': new_pattern_template,
3223                            'file_ext': '.xml'
3224                          },
3225             }
3226
3227 def meta_exists(metatype,
3228                 path_args=None,
3229                 template_args=None,
3230                 create_new=True,
3231                 apiurl=None):
3232
3233     global metatypes
3234
3235     if not apiurl:
3236         apiurl = conf.config['apiurl']
3237     url = make_meta_url(metatype, path_args, apiurl)
3238     try:
3239         data = http_GET(url).readlines()
3240     except urllib2.HTTPError, e:
3241         if e.code == 404 and create_new:
3242             data = metatypes[metatype]['template']
3243             if template_args:
3244                 data = StringIO(data % template_args).readlines()
3245         else:
3246             raise e
3247
3248     return data
3249
3250 def make_meta_url(metatype, path_args=None, apiurl=None, force=False):
3251     global metatypes
3252
3253     if not apiurl:
3254         apiurl = conf.config['apiurl']
3255     if metatype not in metatypes.keys():
3256         raise AttributeError('make_meta_url(): Unknown meta type \'%s\'' % metatype)
3257     path = metatypes[metatype]['path']
3258
3259     if path_args:
3260         path = path % path_args
3261
3262     query = {}
3263     if force:
3264         query = { 'force': '1' }
3265
3266     return makeurl(apiurl, [path], query)
3267
3268
3269 def edit_meta(metatype,
3270               path_args=None,
3271               data=None,
3272               template_args=None,
3273               edit=False,
3274               force=False,
3275               change_is_required=False,
3276               apiurl=None):
3277
3278     global metatypes
3279
3280     if not apiurl:
3281         apiurl = conf.config['apiurl']
3282     if not data:
3283         data = meta_exists(metatype,
3284                            path_args,
3285                            template_args,
3286                            create_new = metatype != 'prjconf', # prjconf always exists, 404 => unknown prj
3287                            apiurl=apiurl)
3288
3289     if edit:
3290         change_is_required = True
3291
3292     url = make_meta_url(metatype, path_args, apiurl, force)
3293     f=metafile(url, data, change_is_required, metatypes[metatype]['file_ext'])
3294
3295     if edit:
3296         f.edit()
3297     else:
3298         f.sync()
3299
3300
3301 def show_files_meta(apiurl, prj, pac, revision=None, expand=False, linkrev=None, linkrepair=False, meta=False):
3302     query = {}
3303     if revision:
3304         query['rev'] = revision
3305     else:
3306         query['rev'] = 'latest'
3307     if linkrev:
3308         query['linkrev'] = linkrev
3309     elif conf.config['linkcontrol']:
3310         query['linkrev'] = 'base'
3311     if meta:
3312         query['meta'] = 1
3313     if expand:
3314         query['expand'] = 1
3315     if linkrepair:
3316        &