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