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