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