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