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