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