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