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