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