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