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