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