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