Make source validator moduler, execute all scripts in directory
[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="checksum").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 = {}, validators = None):
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, validators=validators)
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, validators=validators)
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='', validators=None):
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         if validators:
911             for validator in os.listdir(validators):
912                 import subprocess
913                 p = subprocess.Popen([validators+"/"+validator], close_fds=True)
914                 if p.wait() != 0:
915                     raise oscerr.RuntimeError(p.stdout, validator )
916
917         have_conflicts = False
918         for filename in self.todo:
919             if not filename.startswith('_service:') and not filename.startswith('_service_'):
920                 st = self.status(filename)
921                 if st == 'S':
922                     self.todo.remove(filename)
923                 elif st == 'A' or st == 'M':
924                     self.todo_send.append(filename)
925                     print statfrmt('Sending', os.path.join(pathn, filename))
926                 elif st == 'D':
927                     self.todo_delete.append(filename)
928                     print statfrmt('Deleting', os.path.join(pathn, filename))
929                 elif st == 'C':
930                     have_conflicts = True
931
932         if have_conflicts:
933             print 'Please resolve all conflicts before committing using "osc resolved FILE"!'
934             return 1
935
936         if not self.todo_send and not self.todo_delete and not self.rev == "upload" and not self.islinkrepair() and not self.ispulled():
937             print 'nothing to do for package %s' % self.name
938             return 1
939
940         if self.islink() and self.isexpanded():
941             # resolve the link into the upload revision
942             # XXX: do this always?
943             query = { 'cmd': 'copy', 'rev': 'upload', 'orev': self.rev }
944             u = makeurl(self.apiurl, ['source', self.prjname, self.name], query=query)
945             f = http_POST(u)
946
947         print 'Transmitting file data ',
948         try:
949             for filename in self.todo_delete:
950                 # do not touch local files on commit --
951                 # delete remotely instead
952                 self.delete_remote_source_file(filename)
953                 self.to_be_deleted.remove(filename)
954             for filename in self.todo_send:
955                 sys.stdout.write('.')
956                 sys.stdout.flush()
957                 self.put_source_file(filename)
958
959             # all source files are committed - now comes the log
960             query = { 'cmd'    : 'commit',
961                       'rev'    : 'upload',
962                       'user'   : conf.get_apiurl_usr(self.apiurl),
963                       'comment': msg }
964             if self.islink() and self.isexpanded():
965                 query['keeplink'] = '1'
966                 if conf.config['linkcontrol'] or self.isfrozen():
967                     query['linkrev'] = self.linkinfo.srcmd5
968                 if self.ispulled():
969                     query['repairlink'] = '1'
970                     query['linkrev'] = self.get_pulled_srcmd5()
971             if self.islinkrepair():
972                 query['repairlink'] = '1'
973             u = makeurl(self.apiurl, ['source', self.prjname, self.name], query=query)
974             f = http_POST(u)
975         except Exception, e:
976             # delete upload revision
977             try:
978                 query = { 'cmd': 'deleteuploadrev' }
979                 u = makeurl(self.apiurl, ['source', self.prjname, self.name], query=query)
980                 f = http_POST(u)
981             except:
982                 pass
983             raise e
984
985         root = ET.parse(f).getroot()
986         self.rev = int(root.get('rev'))
987         print
988         print 'Committed revision %s.' % self.rev
989
990         if self.ispulled():
991             os.unlink(os.path.join(self.storedir, '_pulled'))
992         if self.islinkrepair():
993             os.unlink(os.path.join(self.storedir, '_linkrepair'))
994             self.linkrepair = False
995             # XXX: mark package as invalid?
996             print 'The source link has been repaired. This directory can now be removed.'
997         if self.islink() and self.isexpanded():
998             self.update_local_filesmeta(revision=self.latest_rev())
999         else:
1000             self.update_local_filesmeta()
1001         self.write_deletelist()
1002         self.update_datastructs()
1003
1004         if self.filenamelist.count('_service'):
1005             print 'The package contains a source service.'
1006             for filename in self.todo:
1007                 if filename.startswith('_service:') and os.path.exists(filename):
1008                     os.unlink(filename) # remove local files
1009         print_request_list(self.apiurl, self.prjname, self.name)
1010
1011     def write_conflictlist(self):
1012         if len(self.in_conflict) == 0:
1013             try:
1014                 os.unlink(os.path.join(self.storedir, '_in_conflict'))
1015             except:
1016                 pass
1017         else:
1018             fname = os.path.join(self.storedir, '_in_conflict')
1019             f = open(fname, 'w')
1020             f.write('\n'.join(self.in_conflict))
1021             f.write('\n')
1022             f.close()
1023
1024     def updatefile(self, n, revision):
1025         filename = os.path.join(self.dir, n)
1026         storefilename = os.path.join(self.storedir, n)
1027         mtime = self.findfilebyname(n).mtime
1028
1029         get_source_file(self.apiurl, self.prjname, self.name, n, targetfilename=filename,
1030                 revision=revision, progress_obj=self.progress_obj, mtime=mtime)
1031
1032         shutil.copyfile(filename, storefilename)
1033
1034     def mergefile(self, n):
1035         filename = os.path.join(self.dir, n)
1036         storefilename = os.path.join(self.storedir, n)
1037         myfilename = os.path.join(self.dir, n + '.mine')
1038         upfilename = os.path.join(self.dir, n + '.r' + self.rev)
1039         os.rename(filename, myfilename)
1040
1041         mtime = self.findfilebyname(n).mtime
1042         get_source_file(self.apiurl, self.prjname, self.name, n,
1043                         revision=self.rev, targetfilename=upfilename,
1044                         progress_obj=self.progress_obj, mtime=mtime)
1045
1046         if binary_file(myfilename) or binary_file(upfilename):
1047             # don't try merging
1048             shutil.copyfile(upfilename, filename)
1049             shutil.copyfile(upfilename, storefilename)
1050             self.in_conflict.append(n)
1051             self.write_conflictlist()
1052             return 'C'
1053         else:
1054             # try merging
1055             # diff3 OPTIONS... MINE OLDER YOURS
1056             merge_cmd = 'diff3 -m -E %s %s %s > %s' % (myfilename, storefilename, upfilename, filename)
1057             # we would rather use the subprocess module, but it is not availablebefore 2.4
1058             ret = subprocess.call(merge_cmd, shell=True)
1059
1060             #   "An exit status of 0 means `diff3' was successful, 1 means some
1061             #   conflicts were found, and 2 means trouble."
1062             if ret == 0:
1063                 # merge was successful... clean up
1064                 shutil.copyfile(upfilename, storefilename)
1065                 os.unlink(upfilename)
1066                 os.unlink(myfilename)
1067                 return 'G'
1068             elif ret == 1:
1069                 # unsuccessful merge
1070                 shutil.copyfile(upfilename, storefilename)
1071                 self.in_conflict.append(n)
1072                 self.write_conflictlist()
1073                 return 'C'
1074             else:
1075                 print >>sys.stderr, '\ndiff3 got in trouble... exit code:', ret
1076                 print >>sys.stderr, 'the command line was:'
1077                 print >>sys.stderr, merge_cmd
1078                 sys.exit(1)
1079
1080
1081
1082     def update_local_filesmeta(self, revision=None):
1083         """
1084         Update the local _files file in the store.
1085         It is replaced with the version pulled from upstream.
1086         """
1087         meta = ''.join(show_files_meta(self.apiurl, self.prjname, self.name, revision=revision, limit_size=self.limit_size))
1088         store_write_string(self.absdir, '_files', meta)
1089
1090     def update_datastructs(self):
1091         """
1092         Update the internal data structures if the local _files
1093         file has changed (e.g. update_local_filesmeta() has been
1094         called).
1095         """
1096         import fnmatch
1097         files_tree = read_filemeta(self.dir)
1098         files_tree_root = files_tree.getroot()
1099
1100         self.rev = files_tree_root.get('rev')
1101         self.srcmd5 = files_tree_root.get('srcmd5')
1102
1103         self.linkinfo = Linkinfo()
1104         self.linkinfo.read(files_tree_root.find('linkinfo'))
1105
1106         self.filenamelist = []
1107         self.filelist = []
1108         self.skipped = []
1109         for node in files_tree_root.findall('entry'):
1110             try:
1111                 f = File(node.get('name'),
1112                          node.get('md5'),
1113                          int(node.get('size')),
1114                          int(node.get('mtime')))
1115                 if node.get('skipped'):
1116                     self.skipped.append(f.name)
1117             except:
1118                 # okay, a very old version of _files, which didn't contain any metadata yet...
1119                 f = File(node.get('name'), '', 0, 0)
1120             self.filelist.append(f)
1121             self.filenamelist.append(f.name)
1122
1123         self.to_be_deleted = read_tobedeleted(self.dir)
1124         self.in_conflict = read_inconflict(self.dir)
1125         self.linkrepair = os.path.isfile(os.path.join(self.storedir, '_linkrepair'))
1126         self.size_limit = read_sizelimit(self.dir)
1127
1128         # gather unversioned files, but ignore some stuff
1129         self.excluded = [ i for i in os.listdir(self.dir)
1130                           for j in conf.config['exclude_glob']
1131                           if fnmatch.fnmatch(i, j) ]
1132         self.filenamelist_unvers = [ i for i in os.listdir(self.dir)
1133                                      if i not in self.excluded
1134                                      if i not in self.filenamelist ]
1135
1136     def islink(self):
1137         """tells us if the package is a link (has 'linkinfo').
1138         A package with linkinfo is a package which links to another package.
1139         Returns True if the package is a link, otherwise False."""
1140         return self.linkinfo.islink()
1141
1142     def isexpanded(self):
1143         """tells us if the package is a link which is expanded.
1144         Returns True if the package is expanded, otherwise False."""
1145         return self.linkinfo.isexpanded()
1146
1147     def islinkrepair(self):
1148         """tells us if we are repairing a broken source link."""
1149         return self.linkrepair
1150
1151     def ispulled(self):
1152         """tells us if we have pulled a link."""
1153         return os.path.isfile(os.path.join(self.storedir, '_pulled'))
1154
1155     def isfrozen(self):
1156         """tells us if the link is frozen."""
1157         return os.path.isfile(os.path.join(self.storedir, '_frozenlink'))
1158
1159     def get_pulled_srcmd5(self):
1160         pulledrev = None
1161         for line in open(os.path.join(self.storedir, '_pulled'), 'r'):
1162             pulledrev = line.strip()
1163         return pulledrev
1164
1165     def haslinkerror(self):
1166         """
1167         Returns True if the link is broken otherwise False.
1168         If the package is not a link it returns False.
1169         """
1170         return self.linkinfo.haserror()
1171
1172     def linkerror(self):
1173         """
1174         Returns an error message if the link is broken otherwise None.
1175         If the package is not a link it returns None.
1176         """
1177         return self.linkinfo.error
1178
1179     def update_local_pacmeta(self):
1180         """
1181         Update the local _meta file in the store.
1182         It is replaced with the version pulled from upstream.
1183         """
1184         meta = ''.join(show_package_meta(self.apiurl, self.prjname, self.name))
1185         store_write_string(self.absdir, '_meta', meta)
1186
1187     def findfilebyname(self, n):
1188         for i in self.filelist:
1189             if i.name == n:
1190                 return i
1191
1192     def status(self, n):
1193         """
1194         status can be:
1195
1196          file  storefile  file present  STATUS
1197         exists  exists      in _files
1198
1199           x       x            -        'A'
1200           x       x            x        ' ' if digest differs: 'M'
1201                                             and if in conflicts file: 'C'
1202           x       -            -        '?'
1203           x       -            x        'D' and listed in _to_be_deleted
1204           -       x            x        '!'
1205           -       x            -        'D' (when file in working copy is already deleted)
1206           -       -            x        'F' (new in repo, but not yet in working copy)
1207           -       -            -        NOT DEFINED
1208
1209         """
1210
1211         known_by_meta = False
1212         exists = False
1213         exists_in_store = False
1214         if n in self.filenamelist:
1215             known_by_meta = True
1216         if os.path.exists(os.path.join(self.absdir, n)):
1217             exists = True
1218         if os.path.exists(os.path.join(self.storedir, n)):
1219             exists_in_store = True
1220
1221
1222         if n in self.skipped:
1223             state = 'S'
1224         elif exists and not exists_in_store and known_by_meta:
1225             state = 'D'
1226         elif n in self.to_be_deleted:
1227             state = 'D'
1228         elif n in self.in_conflict:
1229             state = 'C'
1230         elif exists and exists_in_store and known_by_meta:
1231             #print self.findfilebyname(n)
1232             if dgst(os.path.join(self.absdir, n)) != self.findfilebyname(n).md5:
1233                 state = 'M'
1234             else:
1235                 state = ' '
1236         elif exists and not exists_in_store and not known_by_meta:
1237             state = '?'
1238         elif exists and exists_in_store and not known_by_meta:
1239             state = 'A'
1240         elif not exists and exists_in_store and known_by_meta:
1241             state = '!'
1242         elif not exists and not exists_in_store and known_by_meta:
1243             state = 'F'
1244         elif not exists and exists_in_store and not known_by_meta:
1245             state = 'D'
1246         elif not exists and not exists_in_store and not known_by_meta:
1247             # this case shouldn't happen (except there was a typo in the filename etc.)
1248             raise IOError('osc: \'%s\' is not under version control' % n)
1249
1250         return state
1251
1252     def comparePac(self, cmp_pac):
1253         """
1254         This method compares the local filelist with
1255         the filelist of the passed package to see which files
1256         were added, removed and changed.
1257         """
1258
1259         changed_files = []
1260         added_files = []
1261         removed_files = []
1262
1263         for file in self.filenamelist+self.filenamelist_unvers:
1264             state = self.status(file)
1265             if file in self.skipped:
1266                 continue
1267             if state == 'A' and (not file in cmp_pac.filenamelist):
1268                 added_files.append(file)
1269             elif file in cmp_pac.filenamelist and state == 'D':
1270                 removed_files.append(file)
1271             elif state == ' ' and not file in cmp_pac.filenamelist:
1272                 added_files.append(file)
1273             elif file in cmp_pac.filenamelist and state != 'A' and state != '?':
1274                 if dgst(os.path.join(self.absdir, file)) != cmp_pac.findfilebyname(file).md5:
1275                     changed_files.append(file)
1276         for file in cmp_pac.filenamelist:
1277             if not file in self.filenamelist:
1278                 removed_files.append(file)
1279         removed_files = set(removed_files)
1280
1281         return changed_files, added_files, removed_files
1282
1283     def merge(self, otherpac):
1284         self.todo += otherpac.todo
1285
1286     def __str__(self):
1287         r = """
1288 name: %s
1289 prjname: %s
1290 workingdir: %s
1291 localfilelist: %s
1292 linkinfo: %s
1293 rev: %s
1294 'todo' files: %s
1295 """ % (self.name,
1296         self.prjname,
1297         self.dir,
1298         '\n               '.join(self.filenamelist),
1299         self.linkinfo,
1300         self.rev,
1301         self.todo)
1302
1303         return r
1304
1305
1306     def read_meta_from_spec(self, spec = None):
1307         import glob
1308         if spec:
1309             specfile = spec
1310         else:
1311             # scan for spec files
1312             speclist = glob.glob(os.path.join(self.dir, '*.spec'))
1313             if len(speclist) == 1:
1314                 specfile = speclist[0]
1315             elif len(speclist) > 1:
1316                 print 'the following specfiles were found:'
1317                 for file in speclist:
1318                     print file
1319                 print 'please specify one with --specfile'
1320                 sys.exit(1)
1321             else:
1322                 print 'no specfile was found - please specify one ' \
1323                       'with --specfile'
1324                 sys.exit(1)
1325
1326         data = read_meta_from_spec(specfile, 'Summary', 'Url', '%description')
1327         self.summary = data['Summary']
1328         self.url = data['Url']
1329         self.descr = data['%description']
1330
1331
1332     def update_package_meta(self, force=False):
1333         """
1334         for the updatepacmetafromspec subcommand
1335             argument force supress the confirm question
1336         """
1337
1338         m = ''.join(show_package_meta(self.apiurl, self.prjname, self.name))
1339
1340         root = ET.fromstring(m)
1341         root.find('title').text = self.summary
1342         root.find('description').text = ''.join(self.descr)
1343         url = root.find('url')
1344         if url == None:
1345             url = ET.SubElement(root, 'url')
1346         url.text = self.url
1347
1348         u = makeurl(self.apiurl, ['source', self.prjname, self.name, '_meta'])
1349         mf = metafile(u, ET.tostring(root))
1350
1351         if not force:
1352             print '*' * 36, 'old', '*' * 36
1353             print m
1354             print '*' * 36, 'new', '*' * 36
1355             print ET.tostring(root)
1356             print '*' * 72
1357             repl = raw_input('Write? (y/N/e) ')
1358         else:
1359             repl = 'y'
1360
1361         if repl == 'y':
1362             mf.sync()
1363         elif repl == 'e':
1364             mf.edit()
1365
1366         mf.discard()
1367
1368     def mark_frozen(self):
1369         store_write_string(self.absdir, '_frozenlink', '')
1370         print
1371         print "The link in this package is currently broken. Checking"
1372         print "out the last working version instead; please use 'osc pull'"
1373         print "to repair the link."
1374         print
1375
1376     def unmark_frozen(self):
1377         if os.path.exists(os.path.join(self.storedir, '_frozenlink')):
1378             os.unlink(os.path.join(self.storedir, '_frozenlink'))
1379
1380     def latest_rev(self):
1381         if self.islinkrepair():
1382             upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrepair=1)
1383         elif self.islink() and self.isexpanded():
1384             if self.isfrozen() or self.ispulled():
1385                 upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev=self.linkinfo.srcmd5)
1386             else:
1387                 try:
1388                     upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name)
1389                 except:
1390                     try:
1391                         upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev=self.linkinfo.srcmd5)
1392                     except:
1393                         upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev="base")
1394                         self.mark_frozen()
1395         else:
1396             upstream_rev = show_upstream_rev(self.apiurl, self.prjname, self.name)
1397         return upstream_rev
1398
1399     def update(self, rev = None, service_files = False, limit_size = None):
1400         # save filelist and (modified) status before replacing the meta file
1401         saved_filenames = self.filenamelist
1402         saved_modifiedfiles = [ f for f in self.filenamelist if self.status(f) == 'M' ]
1403
1404         oldp = self
1405         if limit_size:
1406             self.limit_size = limit_size
1407         else:
1408             self.limit_size = read_sizelimit(self.dir)
1409         self.update_local_filesmeta(rev)
1410         self = Package(self.dir, progress_obj=self.progress_obj)
1411
1412         # which files do no longer exist upstream?
1413         disappeared = [ f for f in saved_filenames if f not in self.filenamelist ]
1414
1415         pathn = getTransActPath(self.dir)
1416
1417         for filename in saved_filenames:
1418             if filename in self.skipped:
1419                 continue
1420             if not filename.startswith('_service:') and filename in disappeared:
1421                 print statfrmt('D', os.path.join(pathn, filename))
1422                 # keep file if it has local modifications
1423                 if oldp.status(filename) == ' ':
1424                     self.delete_localfile(filename)
1425                 self.delete_storefile(filename)
1426
1427         for filename in self.filenamelist:
1428             if filename in self.skipped:
1429                 continue
1430
1431             state = self.status(filename)
1432             if not service_files and filename.startswith('_service:'):
1433                 pass
1434             elif state == 'M' and self.findfilebyname(filename).md5 == oldp.findfilebyname(filename).md5:
1435                 # no merge necessary... local file is changed, but upstream isn't
1436                 pass
1437             elif state == 'M' and filename in saved_modifiedfiles:
1438                 status_after_merge = self.mergefile(filename)
1439                 print statfrmt(status_after_merge, os.path.join(pathn, filename))
1440             elif state == 'M':
1441                 self.updatefile(filename, rev)
1442                 print statfrmt('U', os.path.join(pathn, filename))
1443             elif state == '!':
1444                 self.updatefile(filename, rev)
1445                 print 'Restored \'%s\'' % os.path.join(pathn, filename)
1446             elif state == 'F':
1447                 self.updatefile(filename, rev)
1448                 print statfrmt('A', os.path.join(pathn, filename))
1449             elif state == 'D' and self.findfilebyname(filename).md5 != oldp.findfilebyname(filename).md5:
1450                 self.updatefile(filename, rev)
1451                 self.delete_storefile(filename)
1452                 print statfrmt('U', os.path.join(pathn, filename))
1453             elif state == ' ':
1454                 pass
1455
1456         self.update_local_pacmeta()
1457
1458         #print ljust(p.name, 45), 'At revision %s.' % p.rev
1459         print 'At revision %s.' % self.rev
1460
1461         if not service_files:
1462             self.run_source_services()
1463
1464     def run_source_services(self):
1465         if self.filenamelist.count('_service'):
1466             service = ET.parse(os.path.join(self.absdir, '_service')).getroot()
1467             si = Serviceinfo()
1468             si.read(service)
1469             si.execute(self.absdir)
1470
1471     def prepare_filelist(self):
1472         """Prepare a list of files, which will be processed by process_filelist
1473         method. This allows easy modifications of a file list in commit
1474         phase.
1475         """
1476         if not self.todo:
1477             self.todo = self.filenamelist + self.filenamelist_unvers
1478         self.todo.sort()
1479
1480         ret = ""
1481         for f in [f for f in self.todo if not os.path.isdir(f)]:
1482             action = 'leave'
1483             status = self.status(f)
1484             if status == 'S':
1485                 continue
1486             if status == '!':
1487                 action = 'remove'
1488             ret += "%s %s %s\n" % (action, status, f)
1489
1490         ret += """
1491 # Edit a filelist for package \'%s\'
1492 # Commands:
1493 # l, leave = leave a file as is
1494 # r, remove = remove a file
1495 # a, add   = add a file
1496 #
1497 # If you remove file from a list, it will be unchanged
1498 # If you remove all, commit will be aborted""" % self.name
1499
1500         return ret
1501
1502     def edit_filelist(self):
1503         """Opens a package list in editor for editing. This allows easy
1504         modifications of it just by simple text editing
1505         """
1506
1507         import tempfile
1508         (fd, filename) = tempfile.mkstemp(prefix = 'osc-filelist', suffix = '.txt')
1509         f = os.fdopen(fd, 'w')
1510         f.write(self.prepare_filelist())
1511         f.close()
1512         mtime_orig = os.stat(filename).st_mtime
1513
1514         while 1:
1515             run_editor(filename)
1516             mtime = os.stat(filename).st_mtime
1517             if mtime_orig < mtime:
1518                 filelist = open(filename).readlines()
1519                 os.unlink(filename)
1520                 break
1521             else:
1522                 raise oscerr.UserAbort()
1523
1524         return self.process_filelist(filelist)
1525
1526     def process_filelist(self, filelist):
1527         """Process a filelist - it add/remove or leave files. This depends on
1528         user input. If no file is processed, it raises an ValueError
1529         """
1530
1531         loop = False
1532         for line in [l.strip() for l in filelist if (l[0] != "#" or l.strip() != '')]:
1533
1534             foo = line.split(' ')
1535             if len(foo) == 4:
1536                 action, state, name = (foo[0], ' ', foo[3])
1537             elif len(foo) == 3:
1538                 action, state, name = (foo[0], foo[1], foo[2])
1539             else:
1540                 break
1541             action = action.lower()
1542             loop = True
1543
1544             if action in ('r', 'remove'):
1545                 if self.status(name) == '?':
1546                     os.unlink(name)
1547                     if name in self.todo:
1548                         self.todo.remove(name)
1549                 else:
1550                     self.delete_file(name, True)
1551             elif action in ('a', 'add'):
1552                 if self.status(name) != '?':
1553                     print "Cannot add file %s with state %s, skipped" % (name, self.status(name))
1554                 else:
1555                     self.addfile(name)
1556             elif action in ('l', 'leave'):
1557                 pass
1558             else:
1559                 raise ValueError("Unknow action `%s'" % action)
1560
1561         if not loop:
1562             raise ValueError("Empty filelist")
1563
1564 class ReviewState:
1565     """for objects to represent the review state in a request"""
1566     def __init__(self, state=None, by_user=None, by_group=None, who=None, when=None, comment=None):
1567         self.state = state
1568         self.by_user  = by_user
1569         self.by_group = by_group
1570         self.who  = who
1571         self.when = when
1572         self.comment = comment
1573
1574 class RequestState:
1575     """for objects to represent the "state" of a request"""
1576     def __init__(self, name=None, who=None, when=None, comment=None):
1577         self.name = name
1578         self.who  = who
1579         self.when = when
1580         self.comment = comment
1581
1582 class Action:
1583     """represents an action"""
1584     def __init__(self, type, src_project, src_package, src_rev, dst_project, dst_package, src_update):
1585         self.type = type
1586         self.src_project = src_project
1587         self.src_package = src_package
1588         self.src_rev = src_rev
1589         self.dst_project = dst_project
1590         self.dst_package = dst_package
1591         self.src_update = src_update
1592
1593 class Request:
1594     """represent a request and holds its metadata
1595        it has methods to read in metadata from xml,
1596        different views, ..."""
1597     def __init__(self):
1598         self.reqid       = None
1599         self.state       = RequestState()
1600         self.who         = None
1601         self.when        = None
1602         self.last_author = None
1603         self.descr       = None
1604         self.actions     = []
1605         self.statehistory = []
1606         self.reviews      = []
1607
1608     def read(self, root):
1609         self.reqid = int(root.get('id'))
1610         actions = root.findall('action')
1611         if len(actions) == 0:
1612             actions = [ root.find('submit') ] # for old style requests
1613
1614         for action in actions:
1615             type = action.get('type', 'submit')
1616             try:
1617                 src_prj = src_pkg = src_rev = dst_prj = dst_pkg = src_update = None
1618                 if action.findall('source'):
1619                     n = action.find('source')
1620                     src_prj = n.get('project', None)
1621                     src_pkg = n.get('package', None)
1622                     src_rev = n.get('rev', None)
1623                 if action.findall('target'):
1624                     n = action.find('target')
1625                     dst_prj = n.get('project', None)
1626                     dst_pkg = n.get('package', None)
1627                 if action.findall('options'):
1628                     n = action.find('options')
1629                     if n.findall('sourceupdate'):
1630                         src_update = n.find('sourceupdate').text.strip()
1631                 self.add_action(type, src_prj, src_pkg, src_rev, dst_prj, dst_pkg, src_update)
1632             except:
1633                 msg = 'invalid request format:\n%s' % ET.tostring(root)
1634                 raise oscerr.APIError(msg)
1635
1636         # read the state
1637         n = root.find('state')
1638         self.state.name, self.state.who, self.state.when \
1639                 = n.get('name'), n.get('who'), n.get('when')
1640         try:
1641             self.state.comment = n.find('comment').text.strip()
1642         except:
1643             self.state.comment = None
1644
1645         # read the review states
1646         for r in root.findall('review'):
1647             s = ReviewState()
1648             s.state    = r.get('state')
1649             s.by_user  = r.get('by_user')
1650             s.by_group = r.get('by_group')
1651             s.who      = r.get('who')
1652             s.when     = r.get('when')
1653             try:
1654                 s.comment = r.find('comment').text.strip()
1655             except:
1656                 s.comment = None
1657             self.reviews.append(s)
1658
1659         # read the state history
1660         for h in root.findall('history'):
1661             s = RequestState()
1662             s.name = h.get('name')
1663             s.who  = h.get('who')
1664             s.when = h.get('when')
1665             try:
1666                 s.comment = h.find('comment').text.strip()
1667             except:
1668                 s.comment = None
1669             self.statehistory.append(s)
1670         self.statehistory.reverse()
1671
1672         # read a description, if it exists
1673         try:
1674             n = root.find('description').text
1675             self.descr = n
1676         except:
1677             pass
1678
1679     def add_action(self, type, src_prj, src_pkg, src_rev, dst_prj, dst_pkg, src_update):
1680         self.actions.append(Action(type, src_prj, src_pkg, src_rev,
1681                                    dst_prj, dst_pkg, src_update)
1682                            )
1683
1684     def list_view(self):
1685         ret = '%6d  State:%-7s By:%-12s When:%-12s' % (self.reqid, self.state.name, self.state.who, self.state.when)
1686
1687         for a in self.actions:
1688             dst = "%s/%s" % (a.dst_project, a.dst_package)
1689             if a.src_package == a.dst_package:
1690                 dst = a.dst_project
1691
1692             sr_source=""
1693             if a.type=="submit":
1694                 sr_source="%s/%s  -> " % (a.src_project, a.src_package)
1695             if a.type=="change_devel":
1696                 dst = "developed in %s/%s" % (a.src_project, a.src_package)
1697                 sr_source="%s/%s" % (a.dst_project, a.dst_package)
1698
1699             ret += '\n        %s:       %-50s %-20s   ' % \
1700             (a.type, sr_source, dst)
1701
1702         if self.statehistory and self.statehistory[0]:
1703             who = []
1704             for h in self.statehistory:
1705                 who.append("%s(%s)" % (h.who,h.name))
1706             who.reverse()
1707             ret += "\n        From: %s" % (' -> '.join(who))
1708         if self.descr:
1709             txt = re.sub(r'[^[:isprint:]]', '_', self.descr)
1710             import textwrap
1711             lines = txt.splitlines()
1712             wrapper = textwrap.TextWrapper( width = 80,
1713                     initial_indent='        Descr: ',
1714                     subsequent_indent='               ')
1715             ret += "\n" + wrapper.fill(lines[0])
1716             wrapper.initial_indent = '               '
1717             for line in lines[1:]:
1718                 ret += "\n" + wrapper.fill(line)
1719
1720         ret += "\n"
1721
1722         return ret
1723
1724     def __cmp__(self, other):
1725         return cmp(self.reqid, other.reqid)
1726
1727     def __str__(self):
1728         action_list=""
1729         for action in self.actions:
1730             action_list="  %s:  " % (action.type)
1731             if action.type=="submit":
1732                 r=""
1733                 if action.src_rev:
1734                     r="(r%s)" % (action.src_rev)
1735                 m=""
1736                 if action.src_update:
1737                     m="(%s)" % (action.src_update)
1738                 action_list=action_list+" %s/%s%s%s -> %s" % ( action.src_project, action.src_package, r, m, action.dst_project )
1739                 if action.dst_package:
1740                     action_list=action_list+"/%s" % ( action.dst_package )
1741             elif action.type=="delete":
1742                 action_list=action_list+"  %s" % ( action.dst_project )
1743                 if action.dst_package:
1744                     action_list=action_list+"/%s" % ( action.dst_package )
1745             elif action.type=="change_devel":
1746                 action_list=action_list+" %s/%s developed in %s/%s" % \
1747                            ( action.dst_project, action.dst_package, action.src_project, action.src_package )
1748             action_list=action_list+"\n"
1749
1750         s = """\
1751 Request #%s:
1752
1753 %s
1754
1755 Message:
1756     %s
1757
1758 State:   %-10s   %s %s
1759 Comment: %s
1760 """          % (self.reqid,
1761                action_list,
1762                self.descr,
1763                self.state.name, self.state.when, self.state.who,
1764                self.state.comment)
1765
1766         if len(self.reviews):
1767             reviewitems = [ '%-10s  %s %s %s %s   %s' \
1768                     % (i.state, i.by_user, i.by_group, i.when, i.who, i.comment) \
1769                     for i in self.reviews ]
1770             s += '\nReview:  ' + '\n         '.join(reviewitems)
1771
1772         s += '\n'
1773         if len(self.statehistory):
1774             histitems = [ '%-10s   %s %s' \
1775                     % (i.name, i.when, i.who) \
1776                     for i in self.statehistory ]
1777             s += '\nHistory: ' + '\n         '.join(histitems)
1778
1779         s += '\n'
1780         return s
1781
1782
1783 def shorttime(t):
1784     """format time as Apr 02 18:19
1785     or                Apr 02  2005
1786     depending on whether it is in the current year
1787     """
1788     import time
1789
1790     if time.localtime()[0] == time.localtime(t)[0]:
1791         # same year
1792         return time.strftime('%b %d %H:%M',time.localtime(t))
1793     else:
1794         return time.strftime('%b %d  %Y',time.localtime(t))
1795
1796
1797 def is_project_dir(d):
1798     return os.path.exists(os.path.join(d, store, '_project')) and not \
1799            os.path.exists(os.path.join(d, store, '_package'))
1800
1801
1802 def is_package_dir(d):
1803     return os.path.exists(os.path.join(d, store, '_project')) and \
1804            os.path.exists(os.path.join(d, store, '_package'))
1805
1806 def parse_disturl(disturl):
1807     """Parse a disturl, returns tuple (apiurl, project, source, repository,
1808     revision), else raises an oscerr.WrongArgs exception
1809     """
1810
1811     m = DISTURL_RE.match(disturl)
1812     if not m:
1813         raise oscerr.WrongArgs("`%s' does not look like disturl" % disturl)
1814
1815     apiurl = m.group('apiurl')
1816     if apiurl.split('.')[0] != 'api':
1817         apiurl = 'https://api.' + ".".join(apiurl.split('.')[1:])
1818     return (apiurl, m.group('project'), m.group('source'), m.group('repository'), m.group('revision'))
1819
1820 def parse_buildlogurl(buildlogurl):
1821     """Parse a build log url, returns a tuple (apiurl, project, package,
1822     repository, arch), else raises oscerr.WrongArgs exception"""
1823
1824     global BUILDLOGURL_RE
1825
1826     m = BUILDLOGURL_RE.match(buildlogurl)
1827     if not m:
1828         raise oscerr.WrongArgs('\'%s\' does not look like url with a build log' % buildlogurl)
1829
1830     return (m.group('apiurl'), m.group('project'), m.group('package'), m.group('repository'), m.group('arch'))
1831
1832 def slash_split(l):
1833     """Split command line arguments like 'foo/bar' into 'foo' 'bar'.
1834     This is handy to allow copy/paste a project/package combination in this form.
1835
1836     Trailing slashes are removed before the split, because the split would
1837     otherwise give an additional empty string.
1838     """
1839     r = []
1840     for i in l:
1841         i = i.rstrip('/')
1842         r += i.split('/')
1843     return r
1844
1845 def expand_proj_pack(args, idx=0, howmany=0):
1846     """looks for occurance of '.' at the position idx.
1847     If howmany is 2, both proj and pack are expanded together
1848     using the current directory, or none of them, if not possible.
1849     If howmany is 0, proj is expanded if possible, then, if there
1850     is no idx+1 element in args (or args[idx+1] == '.'), pack is also
1851     expanded, if possible.
1852     If howmany is 1, only proj is expanded if possible.
1853
1854     If args[idx] does not exists, an implicit '.' is assumed.
1855     if not enough elements up to idx exist, an error is raised.
1856
1857     See also parseargs(args), slash_split(args), findpacs(args)
1858     All these need unification, somehow.
1859     """
1860
1861     # print args,idx,howmany
1862
1863     if len(args) < idx:
1864         raise oscerr.WrongArgs('not enough argument, expected at least %d' % idx)
1865
1866     if len(args) == idx:
1867         args += '.'
1868     if args[idx+0] == '.':
1869         if howmany == 0 and len(args) > idx+1:
1870             if args[idx+1] == '.':
1871                 # we have two dots.
1872                 # remove one dot and make sure to expand both proj and pack
1873                 args.pop(idx+1)
1874                 howmany = 2
1875             else:
1876                 howmany = 1
1877         # print args,idx,howmany
1878
1879         args[idx+0] = store_read_project('.')
1880         if howmany == 0:
1881             try:
1882                 package = store_read_package('.')
1883                 args.insert(idx+1, package)
1884             except:
1885                 pass
1886         elif howmany == 2:
1887             package = store_read_package('.')
1888             args.insert(idx+1, package)
1889     return args
1890
1891
1892 def findpacs(files, progress_obj=None):
1893     """collect Package objects belonging to the given files
1894     and make sure each Package is returned only once"""
1895     pacs = []
1896     for f in files:
1897         p = filedir_to_pac(f, progress_obj)
1898         known = None
1899         for i in pacs:
1900             if i.name == p.name:
1901                 known = i
1902                 break
1903         if known:
1904             i.merge(p)
1905         else:
1906             pacs.append(p)
1907     return pacs
1908
1909
1910 def filedir_to_pac(f, progress_obj=None):
1911     """Takes a working copy path, or a path to a file inside a working copy,
1912     and returns a Package object instance
1913
1914     If the argument was a filename, add it onto the "todo" list of the Package """
1915
1916     if os.path.isdir(f):
1917         wd = f
1918         p = Package(wd, progress_obj=progress_obj)
1919     else:
1920         wd = os.path.dirname(f) or os.curdir
1921         p = Package(wd, progress_obj=progress_obj)
1922         p.todo = [ os.path.basename(f) ]
1923     return p
1924
1925
1926 def read_filemeta(dir):
1927     try:
1928         r = ET.parse(os.path.join(dir, store, '_files'))
1929     except SyntaxError, e:
1930         raise oscerr.NoWorkingCopy('\'%s\' is not a valid working copy.\n'
1931                                    'When parsing .osc/_files, the following error was encountered:\n'
1932                                    '%s' % (dir, e))
1933     return r
1934
1935
1936 def read_tobedeleted(dir):
1937     r = []
1938     fname = os.path.join(dir, store, '_to_be_deleted')
1939
1940     if os.path.exists(fname):
1941         r = [ line.strip() for line in open(fname) ]
1942
1943     return r
1944
1945
1946 def read_sizelimit(dir):
1947     r = None
1948     fname = os.path.join(dir, store, '_size_limit')
1949
1950     if os.path.exists(fname):
1951         r = open(fname).readline()
1952
1953     if r is None or not r.isdigit():
1954         return None
1955     return int(r)
1956
1957 def read_inconflict(dir):
1958     r = []
1959     fname = os.path.join(dir, store, '_in_conflict')
1960
1961     if os.path.exists(fname):
1962         r = [ line.strip() for line in open(fname) ]
1963
1964     return r
1965
1966
1967 def parseargs(list_of_args):
1968     """Convenience method osc's commandline argument parsing.
1969
1970     If called with an empty tuple (or list), return a list containing the current directory.
1971     Otherwise, return a list of the arguments."""
1972     if list_of_args:
1973         return list(list_of_args)
1974     else:
1975         return [os.curdir]
1976
1977
1978 def statfrmt(statusletter, filename):
1979     return '%s    %s' % (statusletter, filename)
1980
1981
1982 def pathjoin(a, *p):
1983     """Join two or more pathname components, inserting '/' as needed. Cut leading ./"""
1984     path = os.path.join(a, *p)
1985     if path.startswith('./'):
1986         path = path[2:]
1987     return path
1988
1989
1990 def makeurl(baseurl, l, query=[]):
1991     """Given a list of path compoments, construct a complete URL.
1992
1993     Optional parameters for a query string can be given as a list, as a
1994     dictionary, or as an already assembled string.
1995     In case of a dictionary, the parameters will be urlencoded by this
1996     function. In case of a list not -- this is to be backwards compatible.
1997     """
1998
1999     if conf.config['verbose'] > 1:
2000         print 'makeurl:', baseurl, l, query
2001
2002     if type(query) == type(list()):
2003         query = '&'.join(query)
2004     elif type(query) == type(dict()):
2005         query = urlencode(query)
2006
2007     scheme, netloc = urlsplit(baseurl)[0:2]
2008     return urlunsplit((scheme, netloc, '/'.join(l), query, ''))
2009
2010
2011 def http_request(method, url, headers={}, data=None, file=None, timeout=100):
2012     """wrapper around urllib2.urlopen for error handling,
2013     and to support additional (PUT, DELETE) methods"""
2014
2015     filefd = None
2016
2017     if conf.config['http_debug']:
2018         print
2019         print
2020         print '--', method, url
2021
2022     if method == 'POST' and not file and not data:
2023         # adding data to an urllib2 request transforms it into a POST
2024         data = ''
2025
2026     req = urllib2.Request(url)
2027     api_host_options = {}
2028     try:
2029         api_host_options = conf.get_apiurl_api_host_options(url)
2030         for header, value in api_host_options['http_headers']:
2031             req.add_header(header, value)
2032     except:
2033         # "external" request (url is no apiurl)
2034         pass
2035
2036     req.get_method = lambda: method
2037
2038     # POST requests are application/x-www-form-urlencoded per default
2039     # since we change the request into PUT, we also need to adjust the content type header
2040     if method == 'PUT' or (method == 'POST' and data):
2041         req.add_header('Content-Type', 'application/octet-stream')
2042
2043     if type(headers) == type({}):
2044         for i in headers.keys():
2045             print headers[i]
2046             req.add_header(i, headers[i])
2047
2048     if file and not data:
2049         size = os.path.getsize(file)
2050         if size < 1024*512:
2051             data = open(file, 'rb').read()
2052         else:
2053             import mmap
2054             filefd = open(file, 'rb')
2055             try:
2056                 if sys.platform[:3] != 'win':
2057                     data = mmap.mmap(filefd.fileno(), os.path.getsize(file), mmap.MAP_SHARED, mmap.PROT_READ)
2058                 else:
2059                     data = mmap.mmap(filefd.fileno(), os.path.getsize(file))
2060                 data = buffer(data)
2061             except EnvironmentError, e:
2062                 if e.errno == 19:
2063                     sys.exit('\n\n%s\nThe file \'%s\' could not be memory mapped. It is ' \
2064                              '\non a filesystem which does not support this.' % (e, file))
2065                 elif hasattr(e, 'winerror') and e.winerror == 5:
2066                     # falling back to the default io
2067                     data = open(file, 'rb').read()
2068                 else:
2069                     raise
2070
2071     if conf.config['debug']: print method, url
2072
2073     old_timeout = socket.getdefaulttimeout()
2074     # XXX: dirty hack as timeout doesn't work with python-m2crypto
2075     if old_timeout != timeout and not api_host_options.get('sslcertck'):
2076         socket.setdefaulttimeout(timeout)
2077     try:
2078         fd = urllib2.urlopen(req, data=data)
2079     finally:
2080         if old_timeout != timeout and not api_host_options.get('sslcertck'):
2081             socket.setdefaulttimeout(old_timeout)
2082         if hasattr(conf.cookiejar, 'save'):
2083             conf.cookiejar.save(ignore_discard=True)
2084
2085     if filefd: filefd.close()
2086
2087     return fd
2088
2089
2090 def http_GET(*args, **kwargs):    return http_request('GET', *args, **kwargs)
2091 def http_POST(*args, **kwargs):   return http_request('POST', *args, **kwargs)
2092 def http_PUT(*args, **kwargs):    return http_request('PUT', *args, **kwargs)
2093 def http_DELETE(*args, **kwargs): return http_request('DELETE', *args, **kwargs)
2094
2095
2096 def init_project_dir(apiurl, dir, project):
2097     if not os.path.exists(dir):
2098         if conf.config['checkout_no_colon']:
2099             os.makedirs(dir)      # helpful with checkout_no_colon
2100         else:
2101             os.mkdir(dir)
2102     if not os.path.exists(os.path.join(dir, store)):
2103         os.mkdir(os.path.join(dir, store))
2104
2105     # print 'project=',project,'  dir=',dir
2106     store_write_project(dir, project)
2107     store_write_apiurl(dir, apiurl)
2108     if conf.config['do_package_tracking']:
2109         store_write_initial_packages(dir, project, [])
2110
2111 def init_package_dir(apiurl, project, package, dir, revision=None, files=True, limit_size=None):
2112     if not os.path.isdir(store):
2113         os.mkdir(store)
2114     os.chdir(store)
2115     f = open('_project', 'w')
2116     f.write(project + '\n')
2117     f.close()
2118     f = open('_package', 'w')
2119     f.write(package + '\n')
2120     f.close()
2121
2122     if limit_size:
2123         f = open('_size_limit', 'w')
2124         f.write(str(limit_size))
2125         f.close()
2126
2127     if files:
2128         f = open('_files', 'w')
2129         f.write(''.join(show_files_meta(apiurl, project, package, revision=revision, limit_size=limit_size)))
2130         f.close()
2131     else:
2132         # create dummy
2133         ET.ElementTree(element=ET.Element('directory')).write('_files')
2134
2135     f = open('_osclib_version', 'w')
2136     f.write(__store_version__ + '\n')
2137     f.close()
2138
2139     store_write_apiurl(os.path.pardir, apiurl)
2140
2141     os.chdir(os.pardir)
2142     return
2143
2144
2145 def check_store_version(dir):
2146     versionfile = os.path.join(dir, store, '_osclib_version')
2147     try:
2148         v = open(versionfile).read().strip()
2149     except:
2150         v = ''
2151
2152     if v == '':
2153         msg = 'Error: "%s" is not an osc package working copy.' % os.path.abspath(dir)
2154         if os.path.exists(os.path.join(dir, '.svn')):
2155             msg = msg + '\nTry svn instead of osc.'
2156         raise oscerr.NoWorkingCopy(msg)
2157
2158     if v != __store_version__:
2159         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']:
2160             # version is fine, no migration needed
2161             f = open(versionfile, 'w')
2162             f.write(__store_version__ + '\n')
2163             f.close()
2164             return
2165         msg = 'The osc metadata of your working copy "%s"' % dir
2166         msg += '\nhas __store_version__ = %s, but it should be %s' % (v, __store_version__)
2167         msg += '\nPlease do a fresh checkout or update your client. Sorry about the inconvenience.'
2168         raise oscerr.WorkingCopyWrongVersion, msg
2169
2170
2171 def meta_get_packagelist(apiurl, prj):
2172
2173     u = makeurl(apiurl, ['source', prj])
2174     f = http_GET(u)
2175     root = ET.parse(f).getroot()
2176     return [ node.get('name') for node in root.findall('entry') ]
2177
2178
2179 def meta_get_filelist(apiurl, prj, package, verbose=False, expand=False, revision=None):
2180     """return a list of file names,
2181     or a list File() instances if verbose=True"""
2182
2183     query = {}
2184     if expand:
2185         query['expand'] = 1
2186     if revision:
2187         query['rev'] = revision
2188     else:
2189         query['rev'] = 'latest'
2190
2191     u = makeurl(apiurl, ['source', prj, package], query=query)
2192     f = http_GET(u)
2193     root = ET.parse(f).getroot()
2194
2195     if not verbose:
2196         return [ node.get('name') for node in root.findall('entry') ]
2197
2198     else:
2199         l = []
2200         # rev = int(root.get('rev'))    # don't force int. also allow srcmd5 here.
2201         rev = root.get('rev')
2202         for node in root.findall('entry'):
2203             f = File(node.get('name'),
2204                      node.get('md5'),
2205                      int(node.get('size')),
2206                      int(node.get('mtime')))
2207             f.rev = rev
2208             l.append(f)
2209         return l
2210
2211
2212 def meta_get_project_list(apiurl):
2213     u = makeurl(apiurl, ['source'])
2214     f = http_GET(u)
2215     root = ET.parse(f).getroot()
2216     return sorted([ node.get('name') for node in root ])
2217
2218
2219 def show_project_meta(apiurl, prj):
2220     url = makeurl(apiurl, ['source', prj, '_meta'])
2221     f = http_GET(url)
2222     return f.readlines()
2223
2224
2225 def show_project_conf(apiurl, prj):
2226     url = makeurl(apiurl, ['source', prj, '_config'])
2227     f = http_GET(url)
2228     return f.readlines()
2229
2230
2231 def show_package_trigger_reason(apiurl, prj, pac, repo, arch):
2232     url = makeurl(apiurl, ['build', prj, repo, arch, pac, '_reason'])
2233     try:
2234         f = http_GET(url)
2235         return f.read()
2236     except urllib2.HTTPError, e:
2237         e.osc_msg = 'Error getting trigger reason for project \'%s\' package \'%s\'' % (prj, pac)
2238         raise
2239
2240
2241 def show_package_meta(apiurl, prj, pac):
2242     url = makeurl(apiurl, ['source', prj, pac, '_meta'])
2243     try:
2244         f = http_GET(url)
2245         return f.readlines()
2246     except urllib2.HTTPError, e:
2247         e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
2248         raise
2249
2250
2251 def show_attribute_meta(apiurl, prj, pac, subpac, attribute, with_defaults, with_project):
2252     path=[]
2253     path.append('source')
2254     path.append(prj)
2255     if pac:
2256         path.append(pac)
2257     if pac and subpac:
2258         path.append(subpac)
2259     path.append('_attribute')
2260     if attribute:
2261         path.append(attribute)
2262     query=[]
2263     if with_defaults:
2264         query.append("with_default=1")
2265     if with_project:
2266         query.append("with_project=1")
2267     url = makeurl(apiurl, path, query)
2268     try:
2269         f = http_GET(url)
2270         return f.readlines()
2271     except urllib2.HTTPError, e:
2272         e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
2273         raise
2274
2275
2276 def show_develproject(apiurl, prj, pac):
2277     m = show_package_meta(apiurl, prj, pac)
2278     try:
2279         return ET.fromstring(''.join(m)).find('devel').get('project')
2280     except:
2281         return None
2282
2283
2284 def show_pattern_metalist(apiurl, prj):
2285     url = makeurl(apiurl, ['source', prj, '_pattern'])
2286     try:
2287         f = http_GET(url)
2288         tree = ET.parse(f)
2289     except urllib2.HTTPError, e:
2290         e.osc_msg = 'show_pattern_metalist: Error getting pattern list for project \'%s\'' % prj
2291         raise
2292     r = [ node.get('name') for node in tree.getroot() ]
2293     r.sort()
2294     return r
2295
2296
2297 def show_pattern_meta(apiurl, prj, pattern):
2298     url = makeurl(apiurl, ['source', prj, '_pattern', pattern])
2299     try:
2300         f = http_GET(url)
2301         return f.readlines()
2302     except urllib2.HTTPError, e:
2303         e.osc_msg = 'show_pattern_meta: Error getting pattern \'%s\' for project \'%s\'' % (pattern, prj)
2304         raise
2305
2306
2307 class metafile:
2308     """metafile that can be manipulated and is stored back after manipulation."""
2309     def __init__(self, url, input, change_is_required=False, file_ext='.xml'):
2310         import tempfile
2311
2312         self.url = url
2313         self.change_is_required = change_is_required
2314         (fd, self.filename) = tempfile.mkstemp(prefix = 'osc_metafile.', suffix = file_ext)
2315         f = os.fdopen(fd, 'w')
2316         f.write(''.join(input))
2317         f.close()
2318         self.hash_orig = dgst(self.filename)
2319
2320     def sync(self):
2321         hash = dgst(self.filename)
2322         if self.change_is_required and hash == self.hash_orig:
2323             print 'File unchanged. Not saving.'
2324             os.unlink(self.filename)
2325             return
2326
2327         print 'Sending meta data...'
2328         # don't do any exception handling... it's up to the caller what to do in case
2329         # of an exception
2330         http_PUT(self.url, file=self.filename)
2331         os.unlink(self.filename)
2332         print 'Done.'
2333
2334     def edit(self):
2335         try:
2336             while 1:
2337                 run_editor(self.filename)
2338                 try:
2339                     self.sync()
2340                     break
2341                 except urllib2.HTTPError, e:
2342                     error_help = "%d" % e.code
2343                     if e.headers.get('X-Opensuse-Errorcode'):
2344                         error_help = "%s (%d)" % (e.headers.get('X-Opensuse-Errorcode'), e.code)
2345
2346                     print >>sys.stderr, 'BuildService API error:', error_help
2347                     # examine the error - we can't raise an exception because we might want
2348                     # to try again
2349                     data = e.read()
2350                     if '<summary>' in data:
2351                         print >>sys.stderr, data.split('<summary>')[1].split('</summary>')[0]
2352                     input = raw_input('Try again? ([y/N]): ')
2353                     if input not in ['y', 'Y']:
2354                         break
2355         finally:
2356             self.discard()
2357
2358     def discard(self):
2359         if os.path.exists(self.filename):
2360             print 'discarding %s' % self.filename
2361             os.unlink(self.filename)
2362
2363
2364 # different types of metadata
2365 metatypes = { 'prj':     { 'path': 'source/%s/_meta',
2366                            'template': new_project_templ,
2367                            'file_ext': '.xml'
2368                          },
2369               'pkg':     { 'path'     : 'source/%s/%s/_meta',
2370                            'template': new_package_templ,
2371                            'file_ext': '.xml'
2372                          },
2373               'attribute':     { 'path'     : 'source/%s/%s/_meta',
2374                            'template': new_attribute_templ,
2375                            'file_ext': '.xml'
2376                          },
2377               'prjconf': { 'path': 'source/%s/_config',
2378                            'template': '',
2379                            'file_ext': '.txt'
2380                          },
2381               'user':    { 'path': 'person/%s',
2382                            'template': new_user_template,
2383                            'file_ext': '.xml'
2384                          },
2385               'pattern': { 'path': 'source/%s/_pattern/%s',
2386                            'template': new_pattern_template,
2387                            'file_ext': '.xml'
2388                          },
2389             }
2390
2391 def meta_exists(metatype,
2392                 path_args=None,
2393                 template_args=None,
2394                 create_new=True,
2395                 apiurl=None):
2396
2397     if not apiurl:
2398         apiurl = conf.config['apiurl']
2399     url = make_meta_url(metatype, path_args, apiurl)
2400     try:
2401         data = http_GET(url).readlines()
2402     except urllib2.HTTPError, e:
2403         if e.code == 404 and create_new:
2404             data = metatypes[metatype]['template']
2405             if template_args:
2406                 data = StringIO(data % template_args).readlines()
2407         else:
2408             raise e
2409     return data
2410
2411 def make_meta_url(metatype, path_args=None, apiurl=None):
2412     if not apiurl:
2413         apiurl = conf.config['apiurl']
2414     if metatype not in metatypes.keys():
2415         raise AttributeError('make_meta_url(): Unknown meta type \'%s\'' % metatype)
2416     path = metatypes[metatype]['path']
2417
2418     if path_args:
2419         path = path % path_args
2420
2421     return makeurl(apiurl, [path])
2422
2423
2424 def edit_meta(metatype,
2425               path_args=None,
2426               data=None,
2427               template_args=None,
2428               edit=False,
2429               change_is_required=False,
2430               apiurl=None):
2431
2432     if not apiurl:
2433         apiurl = conf.config['apiurl']
2434     if not data:
2435         data = meta_exists(metatype,
2436                            path_args,
2437                            template_args,
2438                            create_new = metatype != 'prjconf', # prjconf always exists, 404 => unknown prj
2439                            apiurl=apiurl)
2440
2441     if edit:
2442         change_is_required = True
2443
2444     url = make_meta_url(metatype, path_args, apiurl)
2445     f=metafile(url, data, change_is_required, metatypes[metatype]['file_ext'])
2446
2447     if edit:
2448         f.edit()
2449     else:
2450         f.sync()
2451
2452
2453 def show_files_meta(apiurl, prj, pac, revision=None, expand=False, linkrev=None, linkrepair=False, limit_size=None):
2454     query = {}
2455     if revision:
2456         query['rev'] = revision
2457     else:
2458         query['rev'] = 'latest'
2459     if linkrev:
2460         query['linkrev'] = linkrev
2461     elif conf.config['linkcontrol']:
2462         query['linkrev'] = 'base'
2463     if expand:
2464         query['expand'] = 1
2465     if linkrepair:
2466         query['emptylink'] = 1
2467     f = http_GET(makeurl(apiurl, ['source', prj, pac], query=query))
2468
2469     # look for "too large" files according to size limit and mark them
2470     root = ET.fromstring(''.join(f.readlines()))
2471     for e in root.findall('entry'):
2472         size = e.get('size')
2473         if size and limit_size and int(size) > int(limit_size):
2474              e.set('skipped', 'true')
2475     return ET.tostring(root)
2476
2477
2478 def show_upstream_srcmd5(apiurl, prj, pac, expand=False, revision=None):
2479     m = show_files_meta(apiurl, prj, pac, expand=expand, revision=revision)
2480     return ET.fromstring(''.join(m)).get('srcmd5')
2481
2482
2483 def show_upstream_xsrcmd5(apiurl, prj, pac, revision=None, linkrev=None, linkrepair=False):
2484     m = show_files_meta(apiurl, prj, pac, revision=revision, linkrev=linkrev, linkrepair=linkrepair)
2485     try:
2486         # only source link packages have a <linkinfo> element.
2487         li_node = ET.fromstring(''.join(m)).find('linkinfo')
2488     except:
2489         return None
2490
2491     li = Linkinfo()
2492     li.read(li_node)
2493
2494     if li.haserror():
2495         raise oscerr.LinkExpandError(prj, pac, li.error)
2496     return li.xsrcmd5
2497
2498
2499 def show_upstream_rev(apiurl, prj, pac):
2500     m = show_files_meta(apiurl, prj, pac)
2501     return ET.fromstring(''.join(m)).get('rev')
2502
2503
2504 def read_meta_from_spec(specfile, *args):
2505     import codecs, locale, re
2506     """
2507     Read tags and sections from spec file. To read out
2508     a tag the passed argument mustn't end with a colon. To
2509     read out a section the passed argument must start with
2510     a '%'.
2511     This method returns a dictionary which contains the
2512     requested data.
2513     """
2514
2515     if not os.path.isfile(specfile):
2516         raise IOError('\'%s\' is not a regular file' % specfile)
2517
2518     try:
2519         lines = codecs.open(specfile, 'r', locale.getpreferredencoding()).readlines()
2520     except UnicodeDecodeError:
2521         lines = open(specfile).readlines()
2522
2523     tags = []
2524     sections = []
2525     spec_data = {}
2526
2527     for itm in args:
2528         if itm.startswith('%'):
2529             sections.append(itm)
2530         else:
2531             tags.append(itm)
2532
2533     tag_pat = '(?P<tag>^%s)\s*:\s*(?P<val>.*)'
2534     for tag in tags:
2535         m = re.compile(tag_pat % tag, re.I | re.M).search(''.join(lines))
2536         if m and m.group('val'):
2537             spec_data[tag] = m.group('val').strip()
2538         else:
2539             print >>sys.stderr, 'error - tag \'%s\' does not exist' % tag
2540             sys.exit(1)
2541
2542     section_pat = '^%s\s*?$'
2543     for section in sections:
2544         m = re.compile(section_pat % section, re.I | re.M).search(''.join(lines))
2545         if m:
2546             start = lines.index(m.group()+'\n') + 1
2547         else:
2548             print >>sys.stderr, 'error - section \'%s\' does not exist' % section
2549             sys.exit(1)
2550         data = []
2551         for line in lines[start:]:
2552             if line.startswith('%'):
2553                 break
2554             data.append(line)
2555         spec_data[section] = data
2556
2557     return spec_data
2558
2559 def run_pager(message):
2560     import tempfile, sys
2561
2562     if not sys.stdout.isatty():
2563         print message
2564     else:
2565         tmpfile = tempfile.NamedTemporaryFile()
2566         tmpfile.write(message)
2567         tmpfile.flush()
2568         pager = os.getenv('PAGER', default='less')
2569         subprocess.call('%s %s' % (pager, tmpfile.name), shell=True)
2570         tmpfile.close()
2571
2572 def run_editor(filename):
2573     if sys.platform[:3] != 'win':
2574         editor = os.getenv('EDITOR', default='vim')
2575     else:
2576         editor = os.getenv('EDITOR', default='notepad')
2577
2578     return subprocess.call([ editor, filename ])
2579
2580 def edit_message(footer='', template='', templatelen=30):
2581     delim = '--This line, and those below, will be ignored--\n'
2582     import tempfile
2583     (fd, filename) = tempfile.mkstemp(prefix = 'osc-commitmsg', suffix = '.diff')
2584     f = os.fdopen(fd, 'w')
2585     if template != '':
2586         if not templatelen is None:
2587             lines = template.splitlines()
2588             template = '\n'.join(lines[:templatelen])
2589             if lines[templatelen:]:
2590                 footer = '%s\n\n%s' % ('\n'.join(lines[templatelen:]), footer)
2591         f.write(template)
2592     f.write('\n')
2593     f.write(delim)
2594     f.write('\n')
2595     f.write(footer)
2596     f.close()
2597
2598     try:
2599         while 1:
2600             run_editor(filename)
2601             msg = open(filename).read().split(delim)[0].rstrip()
2602
2603             if len(msg):
2604                 break
2605             else:
2606                 input = raw_input('Log message not specified\n'
2607                                   'a)bort, c)ontinue, e)dit: ')
2608                 if input in 'aA':
2609                     raise oscerr.UserAbort()
2610                 elif input in 'cC':
2611                     break
2612                 elif input in 'eE':
2613                     pass
2614     finally:
2615         os.unlink(filename)
2616     return msg
2617
2618
2619 def create_delete_request(apiurl, project, package, message):
2620
2621     import cgi
2622
2623     if package:
2624         package = """package="%s" """ % (package)
2625     else:
2626         package = ""
2627
2628     xml = """\
2629 <request>
2630     <action type="delete">
2631         <target project="%s" %s/>
2632     </action>
2633     <state name="new"/>
2634     <description>%s</description>
2635 </request>
2636 """ % (project, package,
2637        cgi.escape(message or ''))
2638
2639     u = makeurl(apiurl, ['request'], query='cmd=create')
2640     f = http_POST(u, data=xml)
2641
2642     root = ET.parse(f).getroot()
2643     return root.get('id')
2644
2645
2646 def create_change_devel_request(apiurl,
2647                                 devel_project, devel_package,
2648                                 project, package,
2649                                 message):
2650
2651     import cgi
2652     xml = """\
2653 <request>
2654     <action type="change_devel">
2655         <source project="%s" package="%s" />
2656         <target project="%s" package="%s" />
2657     </action>
2658     <state name="new"/>
2659     <description>%s</description>
2660 </request>
2661 """ % (devel_project,
2662        devel_package,
2663        project,
2664        package,
2665        cgi.escape(message or ''))
2666
2667     u = makeurl(apiurl, ['request'], query='cmd=create')
2668     f = http_POST(u, data=xml)
2669
2670     root = ET.parse(f).getroot()
2671     return root.get('id')
2672
2673
2674 # This creates an old style submit request for server api 1.0
2675 def create_submit_request(apiurl,
2676                          src_project, src_package,
2677                          dst_project=None, dst_package=None,
2678                          message=None, orev=None, src_update=None):
2679
2680     import cgi
2681     options_block=""
2682     if src_update:
2683         options_block="""<options><sourceupdate>%s</sourceupdate></options> """ % (src_update)
2684
2685     # Yes, this kind of xml construction is horrible
2686     targetxml = ""
2687     if dst_project:
2688         packagexml = ""
2689         if dst_package:
2690             packagexml = """package="%s" """ %( dst_package )
2691         targetxml = """<target project="%s" %s /> """ %( dst_project, packagexml )
2692     # XXX: keep the old template for now in order to work with old obs instances
2693     xml = """\
2694 <request type="submit">
2695     <submit>
2696         <source project="%s" package="%s" rev="%s"/>
2697         %s
2698         %s
2699     </submit>
2700     <state name="new"/>
2701     <description>%s</description>
2702 </request>
2703 """ % (src_project,
2704        src_package,
2705        orev or show_upstream_rev(apiurl, src_project, src_package),
2706        targetxml,
2707        options_block,
2708        cgi.escape(message or ""))
2709
2710     u = makeurl(apiurl, ['request'], query='cmd=create')
2711     f = http_POST(u, data=xml)
2712
2713     root = ET.parse(f).getroot()
2714     return root.get('id')
2715
2716
2717 def get_request(apiurl, reqid):
2718     u = makeurl(apiurl, ['request', reqid])
2719     f = http_GET(u)
2720     root = ET.parse(f).getroot()
2721
2722     r = Request()
2723     r.read(root)
2724     return r
2725
2726
2727 def change_review_state(apiurl, reqid, newstate, by_user='', by_group='', message='', supersed=''):
2728     u = makeurl(apiurl,
2729                 ['request', reqid],
2730                 query={'cmd': 'changereviewstate', 'newstate': newstate, 'by_user': by_user, 'superseded_by': supersed})
2731     f = http_POST(u, data=message)
2732     return f.read()
2733
2734 def change_request_state(apiurl, reqid, newstate, message='', supersed=''):
2735     u = makeurl(apiurl,
2736                 ['request', reqid],
2737                 query={'cmd': 'changestate', 'newstate': newstate, 'superseded_by': supersed})
2738     f = http_POST(u, data=message)
2739     return f.read()
2740
2741
2742 def get_request_list(apiurl, project='', package='', req_who='', req_state=('new',), req_type=None, exclude_target_projects=[]):
2743     xpath = ''
2744     if not 'all' in req_state:
2745         for state in req_state:
2746             xpath = xpath_join(xpath, 'state/@name=\'%s\'' % state, inner=True)
2747     if req_who:
2748         xpath = xpath_join(xpath, '(state/@who=\'%(who)s\' or history/@who=\'%(who)s\')' % {'who': req_who}, op='and')
2749
2750     # XXX: we cannot use the '|' in the xpath expression because it is not supported
2751     #      in the backend
2752     todo = {}
2753     if project:
2754         todo['project'] = project
2755     if package:
2756         todo['package'] = package
2757     for kind, val in todo.iteritems():
2758         xpath = xpath_join(xpath, '(action/target/@%(kind)s=\'%(val)s\' or ' \
2759                                   'action/source/@%(kind)s=\'%(val)s\' or ' \
2760                                   'submit/target/@%(kind)s=\'%(val)s\' or ' \
2761                                   'submit/source/@%(kind)s=\'%(val)s\')' % {'kind': kind, 'val': val}, op='and')
2762     if req_type:
2763         xpath = xpath_join(xpath, 'action/@type=\'%s\'' % req_type, op='and')
2764     for i in exclude_target_projects:
2765         xpath = xpath_join(xpath, '(not(action/target/@project=\'%(prj)s\' or ' \
2766                                   'submit/target/@project=\'%(prj)s\'))' % {'prj': i}, op='and')
2767
2768     if conf.config['verbose'] > 1:
2769         print '[ %s ]' % xpath
2770     res = search(apiurl, request=xpath)
2771     collection = res['request']
2772     requests = []
2773     for root in collection.findall('request'):
2774         r = Request()
2775         r.read(root)
2776         requests.append(r)
2777     return requests
2778
2779 def get_user_projpkgs_request_list(apiurl, user, req_state=('new',), req_type=None, exclude_projects=[], projpkgs={}):
2780     """Return all new requests for all projects/packages where is user is involved"""
2781     if not projpkgs:
2782         res = get_user_projpkgs(apiurl, user, exclude_projects=exclude_projects)
2783         for i in res['project_id'].findall('project'):
2784             projpkgs[i.get('name')] = []
2785         for i in res['package_id'].findall('package'):
2786             if not i.get('project') in projpkgs.keys():
2787                 projpkgs.setdefault(i.get('project'), []).append(i.get('name'))
2788     xpath = ''
2789     for prj, pacs in projpkgs.iteritems():
2790         if not len(pacs):
2791             xpath = xpath_join(xpath, 'action/target/@project=\'%s\'' % prj, inner=True)
2792         else:
2793             xp = ''
2794             for p in pacs:
2795                 xp = xpath_join(xp, 'action/target/@package=\'%s\'' % p, inner=True)
2796             xp = xpath_join(xp, 'action/target/@project=\'%s\'' % prj, op='and')
2797             xpath = xpath_join(xpath, xp, inner=True)
2798     if req_type:
2799         xpath = xpath_join(xpath, 'action/@type=\'%s\'' % req_type, op='and')
2800     if not 'all' in req_state:
2801         xp = ''
2802         for state in req_state:
2803             xp = xpath_join(xp, 'state/@name=\'%s\'' % state, inner=True)
2804         xpath = xpath_join(xp, '(%s)' % xpath, op='and')
2805     res = search(apiurl, request=xpath)
2806     result = []
2807     for root in res['request'].findall('request'):
2808         r = Request()
2809         r.read(root)
2810         result.append(r)
2811     return result
2812
2813 def get_request_log(apiurl, reqid):
2814     r = get_request(conf.config['apiurl'], reqid)
2815     data = []
2816     frmt = '-' * 76 + '\n%s | %s | %s\n\n%s'
2817     # the description of the request is used for the initial log entry
2818     # otherwise its comment attribute would contain None
2819     if len(r.statehistory) >= 1:
2820         r.statehistory[-1].comment = r.descr
2821     else:
2822         r.state.comment = r.descr
2823     for state in [ r.state ] + r.statehistory:
2824         s = frmt % (state.name, state.who, state.when, str(state.comment))
2825         data.append(s)
2826     return data
2827
2828
2829 def get_user_meta(apiurl, user):
2830     u = makeurl(apiurl, ['person', quote_plus(user)])
2831     try:
2832         f = http_GET(u)
2833         return ''.join(f.readlines())
2834     except urllib2.HTTPError:
2835         print 'user \'%s\' not found' % user
2836         return None
2837
2838
2839 def get_user_data(apiurl, user, *tags):
2840     """get specified tags from the user meta"""
2841     meta = get_user_meta(apiurl, user)
2842     data = []
2843     if meta != None:
2844         root = ET.fromstring(meta)
2845         for tag in tags:
2846             try:
2847                 if root.find(tag).text != None:
2848                     data.append(root.find(tag).text)
2849                 else:
2850                     # tag is empty
2851                     data.append('-')
2852             except AttributeError:
2853                 # this part is reached if the tags tuple contains an invalid tag
2854                 print 'The xml file for user \'%s\' seems to be broken' % user
2855                 return []
2856     return data
2857
2858
2859 def download(url, filename, progress_obj = None, mtime = None):
2860     import tempfile, shutil
2861     o = None
2862     try:
2863         prefix = os.path.basename(filename)
2864         (fd, tmpfile) = tempfile.mkstemp(prefix = prefix, suffix = '.osc')
2865         os.chmod(tmpfile, 0644)
2866         try:
2867             o = os.fdopen(fd, 'wb')
2868             for buf in streamfile(url, http_GET, BUFSIZE, progress_obj=progress_obj):
2869                 o.write(buf)
2870             o.close()
2871             shutil.move(tmpfile, filename)
2872         except:
2873             os.unlink(tmpfile)
2874             raise
2875     finally:
2876         if o is not None:
2877             o.close()
2878
2879     if mtime:
2880         os.utime(filename, (-1, mtime))
2881
2882 def get_source_file(apiurl, prj, package, filename, targetfilename=None, revision=None, progress_obj=None, mtime=None):
2883     targetfilename = targetfilename or filename
2884     query = None
2885     if revision:
2886         query = { 'rev': revision }
2887     u = makeurl(apiurl, ['source', prj, package, pathname2url(filename)], query=query)
2888     download(u, targetfilename, progress_obj, mtime)
2889
2890 def get_binary_file(apiurl, prj, repo, arch,
2891                     filename,
2892                     package = None,
2893                     target_filename = None,
2894                     target_mtime = None,
2895                     progress_meter = False):
2896     progress_obj = None
2897     if progress_meter:
2898         from meter import TextMeter
2899         progress_obj = TextMeter()
2900
2901     target_filename = target_filename or filename
2902
2903     where = package or '_repository'
2904     u = makeurl(apiurl, ['build', prj, repo, arch, where, filename])
2905     download(u, target_filename, progress_obj, target_mtime)
2906
2907 def dgst_from_string(str):
2908     # Python 2.5 depracates the md5 modules
2909     # Python 2.4 doesn't have hashlib yet
2910     try:
2911         import hashlib
2912         md5_hash = hashlib.md5()
2913     except ImportError:
2914         import md5
2915         md5_hash = md5.new()
2916     md5_hash.update(str)
2917     return md5_hash.hexdigest()
2918
2919 def dgst(file):
2920
2921     #if not os.path.exists(file):
2922         #return None
2923
2924     try:
2925         import hashlib
2926         md5 = hashlib
2927     except ImportError:
2928         import md5
2929         md5 = md5
2930     s = md5.md5()
2931     f = open(file, 'rb')
2932     while 1:
2933         buf = f.read(BUFSIZE)
2934         if not buf: break
2935         s.update(buf)
2936     return s.hexdigest()
2937     f.close()
2938
2939
2940 def binary(s):
2941     """return true if a string is binary data using diff's heuristic"""
2942     if s and '\0' in s[:4096]:
2943         return True
2944     return False
2945
2946
2947 def binary_file(fn):
2948     """read 4096 bytes from a file named fn, and call binary() on the data"""
2949     return binary(open(fn, 'rb').read(4096))
2950
2951
2952 def get_source_file_diff(dir, filename, rev, oldfilename = None, olddir = None, origfilename = None):
2953     """
2954     This methods diffs oldfilename against filename (so filename will
2955     be shown as the new file).
2956     The variable origfilename is used if filename and oldfilename differ
2957     in their names (for instance if a tempfile is used for filename etc.)
2958     """
2959
2960     import difflib
2961
2962     if not oldfilename:
2963         oldfilename = filename
2964
2965     if not olddir:
2966         olddir = os.path.join(dir, store)
2967
2968     if not origfilename:
2969         origfilename = filename
2970
2971     file1 = os.path.join(olddir, oldfilename)   # old/stored original
2972     file2 = os.path.join(dir, filename)         # working copy
2973
2974     f1 = open(file1, 'rb')
2975     s1 = f1.read()
2976     f1.close()
2977
2978     f2 = open(file2, 'rb')
2979     s2 = f2.read()
2980     f2.close()
2981
2982     if binary(s1) or binary (s2):
2983         d = ['Binary file %s has changed\n' % origfilename]
2984
2985     else:
2986         d = difflib.unified_diff(\
2987             s1.splitlines(1), \
2988             s2.splitlines(1), \
2989             fromfile = '%s\t(revision %s)' % (origfilename, rev), \
2990             tofile = '%s\t(working copy)' % origfilename)
2991
2992         # if file doesn't end with newline, we need to append one in the diff result
2993         d = list(d)
2994         for i, line in enumerate(d):
2995             if not line.endswith('\n'):
2996                 d[i] += '\n\\ No newline at end of file'
2997                 if i+1 != len(d):
2998                     d[i] += '\n'
2999
3000     return ''.join(d)
3001
3002 def make_diff(wc, revision):
3003     import tempfile
3004     changed_files = []
3005     added_files = []
3006     removed_files = []
3007     cmp_pac = None
3008     diff_hdr = 'Index: %s\n'
3009     diff_hdr += '===================================================================\n'
3010     diff = []
3011     olddir = os.getcwd()
3012     if not revision:
3013         # normal diff
3014         if wc.todo:
3015             for file in wc.todo:
3016                 if file in wc.skipped:
3017                     continue
3018                 if file in wc.filenamelist+wc.filenamelist_unvers:
3019                     state = wc.status(file)
3020                     if state == 'A':
3021                         added_files.append(file)
3022                     elif state == 'D':
3023                         removed_files.append(file)
3024                     elif state == 'M' or state == 'C':
3025                         changed_files.append(file)
3026                 else:
3027                     diff.append('osc: \'%s\' is not under version control' % file)
3028         else:
3029             for file in wc.filenamelist+wc.filenamelist_unvers:
3030                 if file in wc.skipped:
3031                     continue
3032                 state = wc.status(file)
3033                 if state == 'M' or state == 'C':
3034                     changed_files.append(file)
3035                 elif state == 'A':
3036                     added_files.append(file)
3037                 elif state == 'D':
3038                     removed_files.append(file)
3039     else:
3040         tmpdir  = tempfile.mkdtemp(str(revision), wc.name)
3041         os.chdir(tmpdir)
3042         init_package_dir(wc.apiurl, wc.prjname, wc.name, tmpdir, revision)
3043         cmp_pac = Package(tmpdir)
3044         if wc.todo:
3045             for file in wc.todo:
3046                 if file in cmp_pac.skipped:
3047                     continue
3048                 if file in cmp_pac.filenamelist:
3049                     if file in wc.filenamelist:
3050                         changed_files.append(file)
3051                     else:
3052                         diff.append('osc: \'%s\' is not under version control' % file)
3053                 else:
3054                     diff.append('osc: unable to find \'%s\' in revision %s' % (file, cmp_pac.rev))
3055         else:
3056             changed_files, added_files, removed_files = wc.comparePac(cmp_pac)
3057
3058     for file in changed_files:
3059         diff.append(diff_hdr % file)
3060         if cmp_pac == None:
3061             diff.append(get_source_file_diff(wc.absdir, file, wc.rev))
3062         else:
3063             cmp_pac.updatefile(file, revision)
3064             diff.append(get_source_file_diff(wc.absdir, file, revision, file,
3065                                              cmp_pac.absdir, file))
3066     (fd, tmpfile) = tempfile.mkstemp()
3067     for file in added_files:
3068         diff.append(diff_hdr % file)
3069         if cmp_pac == None:
3070             diff.append(get_source_file_diff(wc.absdir, file, wc.rev, os.path.basename(tmpfile),
3071                                              os.path.dirname(tmpfile), file))
3072         else:
3073             diff.append(get_source_file_diff(wc.absdir, file, revision, os.path.basename(tmpfile),
3074                                              os.path.dirname(tmpfile), file))
3075
3076     # FIXME: this is ugly but it cannot be avoided atm
3077     #        if a file is deleted via "osc rm file" we should keep the storefile.
3078     tmp_pac = None
3079     if cmp_pac == None and removed_files:
3080         tmpdir = tempfile.mkdtemp()
3081         os.chdir(tmpdir)
3082         init_package_dir(wc.apiurl, wc.prjname, wc.name, tmpdir, wc.rev)
3083         tmp_pac = Package(tmpdir)
3084         os.chdir(olddir)
3085
3086     for file in removed_files:
3087         diff.append(diff_hdr % file)
3088         if cmp_pac == None:
3089             tmp_pac.updatefile(file, tmp_pac.rev)
3090             diff.append(get_source_file_diff(os.path.dirname(tmpfile), os.path.basename(tmpfile),
3091                                              wc.rev, file, tmp_pac.storedir, file))
3092         else:
3093             cmp_pac.updatefile(file, revision)
3094             diff.append(get_source_file_diff(os.path.dirname(tmpfile), os.path.basename(tmpfile),
3095                                              revision, file, cmp_pac.storedir, file))
3096
3097     os.chdir(olddir)
3098     if cmp_pac != None:
3099         delete_dir(cmp_pac.absdir)
3100     if tmp_pac != None:
3101         delete_dir(tmp_pac.absdir)
3102     return diff
3103
3104
3105 def server_diff(apiurl,
3106                 old_project, old_package, old_revision,
3107                 new_project, new_package, new_revision, unified=False, missingok=False):
3108     query = {'cmd': 'diff', 'expand': '1'}
3109     if old_project:
3110         query['oproject'] = old_project
3111     if old_package:
3112         query['opackage'] = old_package
3113     if old_revision:
3114         query['orev'] = old_revision
3115     if new_revision:
3116         query['rev'] = new_revision
3117     if unified:
3118         query['unified'] = 1
3119     if missingok:
3120         query['missingok'] = 1
3121
3122     u = makeurl(apiurl, ['source', new_project, new_package], query=query)
3123
3124     f = http_POST(u)
3125     return f.read()
3126
3127
3128 def make_dir(apiurl, project, package, pathname=None, prj_dir=None):
3129     """
3130     creates the plain directory structure for a package dir.
3131     The 'apiurl' parameter is needed for the project dir initialization.
3132     The 'project' and 'package' parameters specify the name of the
3133     project and the package. The optional 'pathname' parameter is used
3134     for printing out the message that a new dir was created (default: 'prj_dir/package').
3135     The optional 'prj_dir' parameter specifies the path to the project dir (default: 'project').
3136     """
3137     prj_dir = prj_dir or project
3138
3139     # FIXME: carefully test each patch component of prj_dir,
3140     # if we have a .osc/_files entry at that level.
3141     #   -> if so, we have a package/project clash,
3142     #      and should rename this path component by appending '.proj'
3143     #      and give user a warning message, to discourage such clashes
3144
3145     pathname = pathname or getTransActPath(os.path.join(prj_dir, package))
3146     if is_package_dir(prj_dir):
3147         # we want this to become a project directory,
3148         # but it already is a package directory.
3149         raise oscerr.OscIOError(None, 'checkout_package: package/project clash. Moving myself away not implemented')
3150
3151     if not is_project_dir(prj_dir):
3152         # this directory could exist as a parent direory for one of our earlier
3153         # checked out sub-projects. in this case, we still need to initialize it.
3154         print statfrmt('A', prj_dir)
3155         init_project_dir(apiurl, prj_dir, project)
3156
3157     if is_project_dir(os.path.join(prj_dir, package)):
3158         # the thing exists, but is a project directory and not a package directory
3159         # FIXME: this should be a warning message to discourage package/project clashes
3160         raise oscerr.OscIOError(None, 'checkout_package: package/project clash. Moving project away not implemented')
3161
3162     if not os.path.exists(os.path.join(prj_dir, package)):
3163         print statfrmt('A', pathname)
3164         os.mkdir(os.path.join(prj_dir, package))
3165         os.mkdir(os.path.join(prj_dir, package, store))
3166
3167     return(os.path.join(prj_dir, package))
3168
3169
3170 def checkout_package(apiurl, project, package,
3171                      revision=None, pathname=None, prj_obj=None,
3172                      expand_link=False, prj_dir=None, service_files=None, progress_obj=None, limit_size=None):
3173     try:
3174         # the project we're in might be deleted.
3175         # that'll throw an error then.
3176         olddir = os.getcwd()
3177     except:
3178         olddir = os.environ.get("PWD")
3179
3180     if not prj_dir:
3181         prj_dir = olddir
3182     else:
3183         if sys.platform[:3] == 'win':
3184             prj_dir = prj_dir[:2] + prj_dir[2:].replace(':', ';')
3185         else:
3186             if conf.config['checkout_no_colon']:
3187                 prj_dir = prj_dir.replace(':', '/')
3188
3189     if not pathname:
3190         pathname = getTransActPath(os.path.join(prj_dir, package))
3191
3192     # before we create directories and stuff, check if the package actually
3193     # exists
3194     show_package_meta(apiurl, project, package)
3195
3196     isfrozen = 0
3197     if expand_link:
3198         # try to read from the linkinfo
3199         # if it is a link we use the xsrcmd5 as the revision to be
3200         # checked out
3201         try:
3202             x = show_upstream_xsrcmd5(apiurl, project, package, revision=revision)
3203         except:
3204             x = show_upstream_xsrcmd5(apiurl, project, package, revision=revision, linkrev='base')
3205             if x:
3206                 isfrozen = 1
3207         if x:
3208             revision = x
3209     os.chdir(make_dir(apiurl, project, package, pathname, prj_dir))
3210     init_package_dir(apiurl, project, package, store, revision, limit_size=limit_size)
3211     os.chdir(os.pardir)
3212     p = Package(package, progress_obj=progress_obj)
3213     if isfrozen:
3214         p.mark_frozen()
3215     for filename in p.filenamelist:
3216         if filename in p.skipped:
3217             continue
3218         if service_files or not filename.startswith('_service:'):
3219             p.updatefile(filename, revision)
3220             # print 'A   ', os.path.join(project, package, filename)
3221             print statfrmt('A', os.path.join(pathname, filename))
3222     if conf.config['do_package_tracking']:
3223         # check if we can re-use an existing project object
3224         if prj_obj == None:
3225             prj_obj = Project(os.getcwd())
3226         prj_obj.set_state(p.name, ' ')
3227         prj_obj.write_packages()
3228     os.chdir(olddir)
3229
3230
3231 def replace_pkg_meta(pkgmeta, new_name, new_prj, keep_maintainers = False,
3232                      dst_userid = None, keep_develproject = False):
3233     """
3234     update pkgmeta with new new_name and new_prj and set calling user as the
3235     only maintainer (unless keep_maintainers is set). Additionally remove the
3236     develproject entry (<devel />) unless keep_develproject is true.
3237     """
3238     root = ET.fromstring(''.join(pkgmeta))
3239     root.set('name', new_name)
3240     root.set('project', new_prj)
3241     if not keep_maintainers:
3242         for person in root.findall('person'):
3243             root.remove(person)
3244     if not keep_develproject:
3245         for dp in root.findall('devel'):
3246             root.remove(dp)
3247     return ET.tostring(root)
3248
3249 def link_to_branch(apiurl, project,  package):
3250     """
3251      convert a package with a _link + project.diff to a branch
3252     """
3253
3254     if '_link' in meta_get_filelist(apiurl, project, package):
3255         u = makeurl(apiurl, ['source', project, package], 'cmd=linktobranch')
3256         http_POST(u)
3257     else:
3258         raise oscerr.OscIOError(None, 'no _link file inside project \'%s\' package \'%s\'' % (project, package))
3259
3260 def link_pac(src_project, src_package, dst_project, dst_package, force, rev='', cicount='', disable_publish = False):
3261     """
3262     create a linked package
3263      - "src" is the original package
3264      - "dst" is the "link" package that we are creating here
3265     """
3266     meta_change = False
3267     dst_meta = ''
3268     try:
3269         dst_meta = meta_exists(metatype='pkg',
3270                                path_args=(quote_plus(dst_project), quote_plus(dst_package)),
3271                                template_args=None,
3272                                create_new=False, apiurl=conf.config['apiurl'])
3273         root = ET.fromstring(''.join(dst_meta))
3274         print root.attrib['project']
3275         if root.attrib['project'] != dst_project:
3276            # The source comes from a different project via a project link, we need to create this instance
3277            meta_change = True
3278     except:
3279         meta_change = True
3280
3281     if meta_change:
3282         src_meta = show_package_meta(conf.config['apiurl'], src_project, src_package)
3283         dst_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
3284
3285     if disable_publish:
3286         meta_change = True
3287         root = ET.fromstring(''.join(dst_meta))
3288         elm = root.find('publish')
3289         if not elm:
3290             elm = ET.SubElement(root, 'publish')
3291         elm.clear()
3292         ET.SubElement(elm, 'disable')
3293         dst_meta = ET.tostring(root)
3294
3295     if meta_change:
3296         edit_meta('pkg',
3297                   path_args=(dst_project, dst_package),
3298                   data=dst_meta)
3299     # create the _link file
3300     # but first, make sure not to overwrite an existing one
3301     if '_link' in meta_get_filelist(conf.config['apiurl'], dst_project, dst_package):
3302         if force:
3303             print >>sys.stderr, 'forced overwrite of existing _link file'
3304         else:
3305             print >>sys.stderr
3306             print >>sys.stderr, '_link file already exists...! Aborting'
3307             sys.exit(1)
3308
3309     if rev:
3310         rev = 'rev="%s"' % rev
3311     else:
3312         rev = ''
3313
3314     if cicount:
3315         cicount = 'cicount="%s"' % cicount
3316     else:
3317         cicount = ''
3318
3319     print 'Creating _link...',
3320     link_template = """\
3321 <link project="%s" package="%s" %s %s>
3322 <patches>
3323   <!-- <apply name="patch" /> apply a patch on the source directory  -->
3324   <!-- <topadd>%%define build_with_feature_x 1</topadd> add a line on the top (spec file only) -->
3325   <!-- <add>file.patch</add> add a patch to be applied after %%setup (spec file only) -->
3326   <!-- <delete>filename</delete> delete a file -->
3327 </patches>
3328 </link>
3329 """ % (src_project, src_package, rev, cicount)
3330
3331     u = makeurl(conf.config['apiurl'], ['source', dst_project, dst_package, '_link'])
3332     http_PUT(u, data=link_template)
3333     print 'Done.'
3334
3335 def aggregate_pac(src_project, src_package, dst_project, dst_package, repo_map = {}, disable_publish = False):
3336     """
3337     aggregate package
3338      - "src" is the original package
3339      - "dst" is the "aggregate" package that we are creating here
3340      - "map" is a dictionary SRC => TARGET repository mappings
3341     """
3342     meta_change = False
3343     dst_meta = ''
3344     try:
3345         dst_meta = meta_exists(metatype='pkg',
3346                                path_args=(quote_plus(dst_project), quote_plus(dst_package)),
3347                                template_args=None,
3348                                create_new=False, apiurl=conf.config['apiurl'])
3349     except:
3350         src_meta = show_package_meta(conf.config['apiurl'], src_project, src_package)
3351         dst_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
3352         meta_change = True
3353
3354     if disable_publish:
3355         meta_change = True
3356         root = ET.fromstring(''.join(dst_meta))
3357         elm = root.find('publish')
3358         if not elm:
3359             elm = ET.SubElement(root, 'publish')
3360         elm.clear()
3361         ET.SubElement(elm, 'disable')
3362         dst_meta = ET.tostring(root)
3363     if meta_change:
3364         edit_meta('pkg',
3365                   path_args=(dst_project, dst_package),