- core.py: added filter_role() method + other minor changes
[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
2632 def get_request_log(apiurl, reqid):
2633     r = get_request(conf.config['apiurl'], reqid)
2634     data = []
2635     frmt = '-' * 76 + '\n%s | %s | %s\n\n%s'
2636     # the description of the request is used for the initial log entry
2637     # otherwise its comment attribute would contain None
2638     if len(r.statehistory) >= 1:
2639         r.statehistory[-1].comment = r.descr
2640     else:
2641         r.state.comment = r.descr
2642     for state in [ r.state ] + r.statehistory:
2643         s = frmt % (state.name, state.who, state.when, str(state.comment))
2644         data.append(s)
2645     return data
2646
2647
2648 def get_user_meta(apiurl, user):
2649     u = makeurl(apiurl, ['person', quote_plus(user)])
2650     try:
2651         f = http_GET(u)
2652         return ''.join(f.readlines())
2653     except urllib2.HTTPError:
2654         print 'user \'%s\' not found' % user
2655         return None
2656
2657
2658 def get_user_data(apiurl, user, *tags):
2659     """get specified tags from the user meta"""
2660     meta = get_user_meta(apiurl, user)
2661     data = []
2662     if meta != None:
2663         root = ET.fromstring(meta)
2664         for tag in tags:
2665             try:
2666                 if root.find(tag).text != None:
2667                     data.append(root.find(tag).text)
2668                 else:
2669                     # tag is empty
2670                     data.append('-')
2671             except AttributeError:
2672                 # this part is reached if the tags tuple contains an invalid tag
2673                 print 'The xml file for user \'%s\' seems to be broken' % user
2674                 return []
2675     return data
2676
2677
2678 def get_source_file(apiurl, prj, package, filename, targetfilename=None, revision=None, progress_obj=None):
2679     import tempfile, shutil
2680     query = None
2681     if revision:
2682         query = { 'rev': revision }
2683     o = None
2684     try:
2685         try:
2686             (fd, tmpfile) = tempfile.mkstemp(prefix = filename, suffix = '.osc')
2687             o = os.fdopen(fd, 'wb')
2688             u = makeurl(apiurl, ['source', prj, package, pathname2url(filename)], query=query)
2689             for buf in streamfile(u, http_GET, BUFSIZE, progress_obj=progress_obj):
2690                 o.write(buf)
2691             o.close()
2692             shutil.move(tmpfile, targetfilename or filename)
2693             os.chmod(targetfilename or filename, 0644)
2694         except:
2695             os.unlink(tmpfile)
2696             raise
2697     finally:
2698         if o is not None:
2699             o.close()
2700
2701 def get_binary_file(apiurl, prj, repo, arch,
2702                     filename,
2703                     package = None,
2704                     target_filename = None,
2705                     target_mtime = None,
2706                     progress_meter = False):
2707
2708     target_filename = target_filename or filename
2709
2710     where = package or '_repository'
2711     u = makeurl(apiurl, ['build', prj, repo, arch, where, filename])
2712
2713     if progress_meter:
2714         sys.stdout.write("Downloading %s [  0%%]" % filename)
2715         sys.stdout.flush()
2716
2717     f = http_GET(u)
2718     binsize = int(f.headers['content-length'])
2719
2720     import tempfile
2721     (fd, tmpfilename) = tempfile.mkstemp(prefix = filename + '.', suffix = '.osc')
2722     os.chmod(tmpfilename, 0644)
2723
2724     try:
2725         o = os.fdopen(fd, 'wb')
2726
2727         downloaded = 0
2728         while 1:
2729             #buf = f.read(BUFSIZE)
2730             buf = f.read(16384)
2731             if not buf: break
2732             o.write(buf)
2733             downloaded += len(buf)
2734             if progress_meter:
2735                 completion = str(int((float(downloaded)/binsize)*100))
2736                 sys.stdout.write('%s%*s%%]' % ('\b'*5, 3, completion))
2737                 sys.stdout.flush()
2738         o.close()
2739
2740         if progress_meter:
2741             sys.stdout.write('\n')
2742
2743         shutil.move(tmpfilename, target_filename)
2744         if target_mtime:
2745             os.utime(target_filename, (-1, target_mtime))
2746
2747     # make sure that the temp file is cleaned up when we are interrupted
2748     finally:
2749         try: os.unlink(tmpfilename)
2750         except: pass
2751
2752 def dgst_from_string(str):
2753     # Python 2.5 depracates the md5 modules
2754     # Python 2.4 doesn't have hashlib yet
2755     try:
2756         import hashlib
2757         md5_hash = hashlib.md5()
2758     except ImportError:
2759         import md5
2760         md5_hash = md5.new()
2761     md5_hash.update(str)
2762     return md5_hash.hexdigest()
2763
2764 def dgst(file):
2765
2766     #if not os.path.exists(file):
2767         #return None
2768
2769     try:
2770         import hashlib
2771         md5 = hashlib
2772     except ImportError:
2773         import md5
2774         md5 = md5
2775     s = md5.md5()
2776     f = open(file, 'rb')
2777     while 1:
2778         buf = f.read(BUFSIZE)
2779         if not buf: break
2780         s.update(buf)
2781     return s.hexdigest()
2782     f.close()
2783
2784
2785 def binary(s):
2786     """return true if a string is binary data using diff's heuristic"""
2787     if s and '\0' in s[:4096]:
2788         return True
2789     return False
2790
2791
2792 def binary_file(fn):
2793     """read 4096 bytes from a file named fn, and call binary() on the data"""
2794     return binary(open(fn, 'rb').read(4096))
2795
2796
2797 def get_source_file_diff(dir, filename, rev, oldfilename = None, olddir = None, origfilename = None):
2798     """
2799     This methods diffs oldfilename against filename (so filename will
2800     be shown as the new file).
2801     The variable origfilename is used if filename and oldfilename differ
2802     in their names (for instance if a tempfile is used for filename etc.)
2803     """
2804
2805     import difflib
2806
2807     if not oldfilename:
2808         oldfilename = filename
2809
2810     if not olddir:
2811         olddir = os.path.join(dir, store)
2812
2813     if not origfilename:
2814         origfilename = filename
2815
2816     file1 = os.path.join(olddir, oldfilename)   # old/stored original
2817     file2 = os.path.join(dir, filename)         # working copy
2818
2819     f1 = open(file1, 'rb')
2820     s1 = f1.read()
2821     f1.close()
2822
2823     f2 = open(file2, 'rb')
2824     s2 = f2.read()
2825     f2.close()
2826
2827     if binary(s1) or binary (s2):
2828         d = ['Binary file %s has changed\n' % origfilename]
2829
2830     else:
2831         d = difflib.unified_diff(\
2832             s1.splitlines(1), \
2833             s2.splitlines(1), \
2834             fromfile = '%s\t(revision %s)' % (origfilename, rev), \
2835             tofile = '%s\t(working copy)' % origfilename)
2836
2837         # if file doesn't end with newline, we need to append one in the diff result
2838         d = list(d)
2839         for i, line in enumerate(d):
2840             if not line.endswith('\n'):
2841                 d[i] += '\n\\ No newline at end of file'
2842                 if i+1 != len(d):
2843                     d[i] += '\n'
2844
2845     return ''.join(d)
2846
2847 def make_diff(wc, revision):
2848     import tempfile
2849     changed_files = []
2850     added_files = []
2851     removed_files = []
2852     cmp_pac = None
2853     diff_hdr = 'Index: %s\n'
2854     diff_hdr += '===================================================================\n'
2855     diff = []
2856     olddir = os.getcwd()
2857     if not revision:
2858         # normal diff
2859         if wc.todo:
2860             for file in wc.todo:
2861                 if file in wc.filenamelist+wc.filenamelist_unvers:
2862                     state = wc.status(file)
2863                     if state == 'A':
2864                         added_files.append(file)
2865                     elif state == 'D':
2866                         removed_files.append(file)
2867                     elif state == 'M' or state == 'C':
2868                         changed_files.append(file)
2869                 else:
2870                     diff.append('osc: \'%s\' is not under version control' % file)
2871         else:
2872             for file in wc.filenamelist+wc.filenamelist_unvers:
2873                 state = wc.status(file)
2874                 if state == 'M' or state == 'C':
2875                     changed_files.append(file)
2876                 elif state == 'A':
2877                     added_files.append(file)
2878                 elif state == 'D':
2879                     removed_files.append(file)
2880     else:
2881         tmpdir  = tempfile.mkdtemp(str(revision), wc.name)
2882         os.chdir(tmpdir)
2883         init_package_dir(wc.apiurl, wc.prjname, wc.name, tmpdir, revision)
2884         cmp_pac = Package(tmpdir)
2885         if wc.todo:
2886             for file in wc.todo:
2887                 if file in cmp_pac.filenamelist:
2888                     if file in wc.filenamelist:
2889                         changed_files.append(file)
2890                     else:
2891                         diff.append('osc: \'%s\' is not under version control' % file)
2892                 else:
2893                     diff.append('osc: unable to find \'%s\' in revision %s' % (file, cmp_pac.rev))
2894         else:
2895             changed_files, added_files, removed_files = wc.comparePac(cmp_pac)
2896
2897     for file in changed_files:
2898         diff.append(diff_hdr % file)
2899         if cmp_pac == None:
2900             diff.append(get_source_file_diff(wc.absdir, file, wc.rev))
2901         else:
2902             cmp_pac.updatefile(file, revision)
2903             diff.append(get_source_file_diff(wc.absdir, file, revision, file,
2904                                              cmp_pac.absdir, file))
2905     (fd, tmpfile) = tempfile.mkstemp()
2906     for file in added_files:
2907         diff.append(diff_hdr % file)
2908         if cmp_pac == None:
2909             diff.append(get_source_file_diff(wc.absdir, file, wc.rev, os.path.basename(tmpfile),
2910                                              os.path.dirname(tmpfile), file))
2911         else:
2912             diff.append(get_source_file_diff(wc.absdir, file, revision, os.path.basename(tmpfile),
2913                                              os.path.dirname(tmpfile), file))
2914
2915     # FIXME: this is ugly but it cannot be avoided atm
2916     #        if a file is deleted via "osc rm file" we should keep the storefile.
2917     tmp_pac = None
2918     if cmp_pac == None and removed_files:
2919         tmpdir = tempfile.mkdtemp()
2920         os.chdir(tmpdir)
2921         init_package_dir(wc.apiurl, wc.prjname, wc.name, tmpdir, wc.rev)
2922         tmp_pac = Package(tmpdir)
2923         os.chdir(olddir)
2924
2925     for file in removed_files:
2926         diff.append(diff_hdr % file)
2927         if cmp_pac == None:
2928             tmp_pac.updatefile(file, tmp_pac.rev)
2929             diff.append(get_source_file_diff(os.path.dirname(tmpfile), os.path.basename(tmpfile),
2930                                              wc.rev, file, tmp_pac.storedir, file))
2931         else:
2932             cmp_pac.updatefile(file, revision)
2933             diff.append(get_source_file_diff(os.path.dirname(tmpfile), os.path.basename(tmpfile),
2934                                              revision, file, cmp_pac.storedir, file))
2935
2936     os.chdir(olddir)
2937     if cmp_pac != None:
2938         delete_dir(cmp_pac.absdir)
2939     if tmp_pac != None:
2940         delete_dir(tmp_pac.absdir)
2941     return diff
2942
2943
2944 def server_diff(apiurl,
2945                 old_project, old_package, old_revision,
2946                 new_project, new_package, new_revision, unified=False):
2947
2948     query = {'cmd': 'diff', 'expand': '1'}
2949     if old_project:
2950         query['oproject'] = old_project
2951     if old_package:
2952         query['opackage'] = old_package
2953     if old_revision:
2954         query['orev'] = old_revision
2955     if new_revision:
2956         query['rev'] = new_revision
2957     if unified:
2958         query['unified'] = 1
2959
2960     u = makeurl(apiurl, ['source', new_project, new_package], query=query)
2961
2962     f = http_POST(u)
2963     return f.read()
2964
2965
2966 def make_dir(apiurl, project, package, pathname=None, prj_dir=None):
2967     """
2968     creates the plain directory structure for a package dir.
2969     The 'apiurl' parameter is needed for the project dir initialization.
2970     The 'project' and 'package' parameters specify the name of the
2971     project and the package. The optional 'pathname' parameter is used
2972     for printing out the message that a new dir was created (default: 'prj_dir/package').
2973     The optional 'prj_dir' parameter specifies the path to the project dir (default: 'project').
2974     """
2975     prj_dir = prj_dir or project
2976
2977     # FIXME: carefully test each patch component of prj_dir,
2978     # if we have a .osc/_files entry at that level.
2979     #   -> if so, we have a package/project clash,
2980     #      and should rename this path component by appending '.proj'
2981     #      and give user a warning message, to discourage such clashes
2982
2983     pathname = pathname or getTransActPath(os.path.join(prj_dir, package))
2984     if is_package_dir(prj_dir):
2985         # we want this to become a project directory,
2986         # but it already is a package directory.
2987         raise oscerr.OscIOError(None, 'checkout_package: package/project clash. Moving myself away not implemented')
2988
2989     if not is_project_dir(prj_dir):
2990         # this directory could exist as a parent direory for one of our earlier
2991         # checked out sub-projects. in this case, we still need to initialize it.
2992         print statfrmt('A', prj_dir)
2993         init_project_dir(apiurl, prj_dir, project)
2994
2995     if is_project_dir(os.path.join(prj_dir, package)):
2996         # the thing exists, but is a project directory and not a package directory
2997         # FIXME: this should be a warning message to discourage package/project clashes
2998         raise oscerr.OscIOError(None, 'checkout_package: package/project clash. Moving project away not implemented')
2999
3000     if not os.path.exists(os.path.join(prj_dir, package)):
3001         print statfrmt('A', pathname)
3002         os.mkdir(os.path.join(prj_dir, package))
3003         os.mkdir(os.path.join(prj_dir, package, store))
3004
3005     return(os.path.join(prj_dir, package))
3006
3007
3008 def checkout_package(apiurl, project, package,
3009                      revision=None, pathname=None, prj_obj=None,
3010                      expand_link=False, prj_dir=None, service_files=None, progress_obj=None):
3011     try:
3012         # the project we're in might be deleted.
3013         # that'll throw an error then.
3014         olddir = os.getcwd()
3015     except:
3016         olddir = os.environ.get("PWD")
3017
3018     if not prj_dir:
3019         prj_dir = olddir
3020     else:
3021         if sys.platform[:3] == 'win':
3022             prj_dir = prj_dir[:2] + prj_dir[2:].replace(':', ';')
3023         else:
3024             if conf.config['checkout_no_colon']:
3025                 prj_dir = prj_dir.replace(':', '/')
3026
3027     if not pathname:
3028         pathname = getTransActPath(os.path.join(prj_dir, package))
3029
3030     # before we create directories and stuff, check if the package actually
3031     # exists
3032     show_package_meta(apiurl, project, package)
3033
3034     isfrozen = 0
3035     if expand_link:
3036         # try to read from the linkinfo
3037         # if it is a link we use the xsrcmd5 as the revision to be
3038         # checked out
3039         try:
3040             x = show_upstream_xsrcmd5(apiurl, project, package, revision=revision)
3041         except:
3042             x = show_upstream_xsrcmd5(apiurl, project, package, revision=revision, linkrev='base')
3043             if x:
3044                 isfrozen = 1
3045         if x:
3046             revision = x
3047     os.chdir(make_dir(apiurl, project, package, pathname, prj_dir))
3048     init_package_dir(apiurl, project, package, store, revision)
3049     os.chdir(os.pardir)
3050     p = Package(package, progress_obj=progress_obj)
3051     if isfrozen:
3052         p.mark_frozen()
3053     for filename in p.filenamelist:
3054         if service_files or not filename.startswith('_service:'):
3055             p.updatefile(filename, revision)
3056             # print 'A   ', os.path.join(project, package, filename)
3057             print statfrmt('A', os.path.join(pathname, filename))
3058     if conf.config['do_package_tracking']:
3059         # check if we can re-use an existing project object
3060         if prj_obj == None:
3061             prj_obj = Project(os.getcwd())
3062         prj_obj.set_state(p.name, ' ')
3063         prj_obj.write_packages()
3064     os.chdir(olddir)
3065
3066
3067 def replace_pkg_meta(pkgmeta, new_name, new_prj, keep_maintainers = False,
3068                      dst_userid = None, keep_develproject = False):
3069     """
3070     update pkgmeta with new new_name and new_prj and set calling user as the
3071     only maintainer (unless keep_maintainers is set). Additionally remove the
3072     develproject entry (<devel />) unless keep_develproject is true.
3073     """
3074     root = ET.fromstring(''.join(pkgmeta))
3075     root.set('name', new_name)
3076     root.set('project', new_prj)
3077     if not keep_maintainers:
3078         for person in root.findall('person'):
3079             root.remove(person)
3080     if not keep_develproject:
3081         for dp in root.findall('devel'):
3082             root.remove(dp)
3083     return ET.tostring(root)
3084
3085 def link_to_branch(apiurl, project,  package):
3086     """
3087      convert a package with a _link + project.diff to a branch
3088     """
3089
3090     if '_link' in meta_get_filelist(apiurl, project, package):
3091         u = makeurl(apiurl, ['source', project, package], 'cmd=linktobranch')
3092         http_POST(u)
3093     else:
3094         raise oscerr.OscIOError(None, 'no _link file inside project \'%s\' package \'%s\'' % (project, package))
3095
3096 def link_pac(src_project, src_package, dst_project, dst_package, force, rev='', cicount='', disable_publish = False):
3097     """
3098     create a linked package
3099      - "src" is the original package
3100      - "dst" is the "link" package that we are creating here
3101     """
3102     meta_change = False
3103     dst_meta = ''
3104     try:
3105         dst_meta = meta_exists(metatype='pkg',
3106                                path_args=(quote_plus(dst_project), quote_plus(dst_package)),
3107                                template_args=None,
3108                                create_new=False, apiurl=conf.config['apiurl'])
3109     except:
3110         src_meta = show_package_meta(conf.config['apiurl'], src_project, src_package)
3111         dst_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
3112         meta_change = True
3113
3114     if disable_publish:
3115         meta_change = True
3116         root = ET.fromstring(''.join(dst_meta))
3117         elm = root.find('publish')
3118         if not elm:
3119             elm = ET.SubElement(root, 'publish')
3120         elm.clear()
3121         ET.SubElement(elm, 'disable')
3122         dst_meta = ET.tostring(root)
3123     if meta_change:
3124         edit_meta('pkg',
3125                   path_args=(dst_project, dst_package),
3126                   data=dst_meta)
3127     # create the _link file
3128     # but first, make sure not to overwrite an existing one
3129     if '_link' in meta_get_filelist(conf.config['apiurl'], dst_project, dst_package):
3130         if force:
3131             print >>sys.stderr, 'forced overwrite of existing _link file'
3132         else:
3133             print >>sys.stderr
3134             print >>sys.stderr, '_link file already exists...! Aborting'
3135             sys.exit(1)
3136
3137     if rev:
3138         rev = 'rev="%s"' % rev
3139     else:
3140         rev = ''
3141
3142     if cicount:
3143         cicount = 'cicount="%s"' % cicount
3144     else:
3145         cicount = ''
3146
3147     print 'Creating _link...',
3148     link_template = """\
3149 <link project="%s" package="%s" %s %s>
3150 <patches>
3151   <!-- <apply name="patch" /> apply a patch on the source directory  -->
3152   <!-- <topadd>%%define build_with_feature_x 1</topadd> add a line on the top (spec file only) -->
3153   <!-- <add>file.patch</add> add a patch to be applied after %%setup (spec file only) -->
3154   <!-- <delete>filename</delete> delete a file -->
3155 </patches>
3156 </link>
3157 """ % (src_project, src_package, rev, cicount)
3158
3159     u = makeurl(conf.config['apiurl'], ['source', dst_project, dst_package, '_link'])
3160     http_PUT(u, data=link_template)
3161     print 'Done.'
3162
3163 def aggregate_pac(src_project, src_package, dst_project, dst_package, repo_map = {}, disable_publish = False):
3164     """
3165     aggregate package
3166      - "src" is the original package
3167      - "dst" is the "aggregate" package that we are creating here
3168      - "map" is a dictionary SRC => TARGET repository mappings
3169     """
3170     meta_change = False
3171     dst_meta = ''
3172     try:
3173         dst_meta = meta_exists(metatype='pkg',
3174                                path_args=(quote_plus(dst_project), quote_plus(dst_package)),
3175                                template_args=None,
3176                                create_new=False, apiurl=conf.config['apiurl'])
3177     except:
3178         src_meta = show_package_meta(conf.config['apiurl'], src_project, src_package)
3179         dst_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
3180         meta_change = True
3181
3182     if disable_publish:
3183         meta_change = True
3184         root = ET.fromstring(''.join(dst_meta))
3185         elm = root.find('publish')
3186         if not elm:
3187             elm = ET.SubElement(root, 'publish')
3188         elm.clear()
3189         ET.SubElement(elm, 'disable')
3190         dst_meta = ET.tostring(root)
3191     if meta_change:
3192         edit_meta('pkg',
3193                   path_args=(dst_project, dst_package),
3194                   data=dst_meta)
3195
3196     # create the _aggregate file
3197     # but first, make sure not to overwrite an existing one
3198     if '_aggregate' in meta_get_filelist(conf.config['apiurl'], dst_project, dst_package):
3199         print >>sys.stderr
3200         print >>sys.stderr, '_aggregate file already exists...! Aborting'
3201         sys.exit(1)
3202
3203     print 'Creating _aggregate...',
3204     aggregate_template = """\
3205 <aggregatelist>
3206   <aggregate project="%s">
3207 """ % (src_project)
3208     for tgt, src in repo_map.iteritems():
3209         aggregate_template += """\
3210     <repository target="%s" source="%s" />
3211 """ % (tgt, src)
3212
3213     aggregate_template += """\
3214     <package>%s</package>
3215   </aggregate>
3216 </aggregatelist>
3217 """ % ( src_package)
3218
3219     u = makeurl(conf.config['apiurl'], ['source', dst_project, dst_package, '_aggregate'])
3220     http_PUT(u, data=aggregate_template)
3221     print 'Done.'
3222
3223
3224 def attribute_branch_pkg(apiurl, attribute, maintained_update_project_attribute, package, targetproject, return_existing=False):
3225     """
3226     Branch packages defined via attributes (via API call)
3227     """
3228     query = { 'cmd': 'branch' }
3229     query['attribute'] = attribute
3230     if targetproject:
3231         query['target_project'] = targetproject
3232     if package:
3233         query['package'] = package
3234     if maintained_update_project_attribute:
3235         query['update_project_attribute'] = maintained_update_project_attribute
3236
3237     u = makeurl(apiurl, ['source'], query=query)
3238     f = None
3239     try:
3240         f = http_POST(u)
3241     except urllib2.HTTPError, e:
3242         msg = ''.join(e.readlines())
3243         msg = msg.split('<summary>')[1]
3244         msg = msg.split('</summary>')[0]
3245         m = re.match(r"attribute branch call failed: (\S+)/", msg)
3246         return m
3247
3248     r = f.read()
3249     r = r.split('targetproject">')[1]
3250     r = r.split('</data>')[0]
3251     return r
3252
3253
3254 def branch_pkg(apiurl, src_project, src_package, nodevelproject=False, rev=None, target_project=None, target_package=None, return_existing=False, msg=''):
3255     """
3256     Branch a package (via API call)
3257     """
3258     query = { 'cmd': 'branch' }
3259     if nodevelproject:
3260         query['ignoredevel'] = '1'
3261     if rev:
3262         query['rev'] = rev
3263     if target_project:
3264         query['target_project'] = target_project
3265     if target_package:
3266         query['target_package'] = target_package
3267     if msg:
3268         query['comment'] = msg
3269     u = makeurl(apiurl, ['source', src_project, src_package], query=query)
3270     try:
3271         f = http_POST(u)
3272     except urllib2.HTTPError, e:
3273         if not return_existing:
3274             raise
3275         msg = ''.join(e.readlines())
3276         msg = msg.split('<summary>')[1]
3277         msg = msg.split('</summary>')[0]
3278         m = re.match(r"branch target package already exists: (\S+)/(\S+)", msg)
3279         if not m:
3280             e.msg += '\n' + msg
3281             raise
3282         return (True, m.group(1), m.group(2), None, None)
3283
3284     data = {}
3285     for i in ET.fromstring(f.read()).findall('data'):
3286         data[i.get('name')] = i.text
3287     return (False, data.get('targetproject', None), data.get('targetpackage', None),
3288             data.get('sourceproject', None), data.get('sourcepackage', None))
3289
3290
3291 def copy_pac(src_apiurl, src_project, src_package,
3292              dst_apiurl, dst_project, dst_package,
3293              client_side_copy = False,
3294              keep_maintainers = False,
3295              keep_develproject = False,
3296              expand = False,
3297              revision = None,
3298              comment = None):
3299     """
3300     Create a copy of a package.
3301
3302     Copying can be done by downloading the files from one package and commit
3303     them into the other by uploading them (client-side copy) --
3304     or by the server, in a single api call.
3305     """
3306
3307     src_meta = show_package_meta(src_apiurl, src_project, src_package)
3308     dst_userid = conf.get_apiurl_usr(dst_apiurl)
3309     src_meta = replace_pkg_meta(src_meta, dst_package, dst_project, keep_maintainers,
3310                                 dst_userid, keep_develproject)
3311
3312     print 'Sending meta data...'
3313     u = makeurl(dst_apiurl, ['source', dst_project, dst_package, '_meta'])
3314     http_PUT(u, data=src_meta)
3315
3316     print 'Copying files...'
3317     if not client_side_copy:
3318         query = {'cmd': 'copy', 'oproject': src_project, 'opackage': src_package }
3319         if expand:
3320             query['expand'] = '1'
3321         if revision:
3322             query['orev'] = revision
3323         if comment:
3324             query['comment'] = comment
3325         u = makeurl(dst_apiurl, ['source', dst_project, dst_package], query=query)
3326         f = http_POST(u)
3327         return f.read()
3328
3329     else:
3330         # copy one file after the other
3331         import tempfile
3332         tmpdir = tempfile.mkdtemp(prefix='osc_copypac')
3333         os.chdir(tmpdir)
3334         query = {'rev': 'upload'}
3335         for n in meta_get_filelist(src_apiurl, src_project, src_package, expand=expand):
3336             print '  ', n
3337             get_source_file(src_apiurl, src_project, src_package, n, targetfilename=n, revision=revision)
3338             u = makeurl(dst_apiurl, ['source', dst_project, dst_package, pathname2url(n)], query=query)
3339             http_PUT(u, file = n)
3340             os.unlink(n)
3341         if comment:
3342             query['comment'] = comment
3343         query['cmd'] = 'commit'
3344         u = makeurl(dst_apiurl, ['source', dst_project, dst_package], query=query)
3345         http_POST(u)
3346         os.rmdir(tmpdir)
3347         return 'Done.'
3348
3349
3350 def delete_package(apiurl, prj, pac):
3351     u = makeurl(apiurl, ['source', prj, pac])
3352     http_DELETE(u)
3353
3354
3355 def delete_project(apiurl, prj):
3356     u = makeurl(apiurl, ['source', prj])
3357     http_DELETE(u)
3358
3359 def delete_files(apiurl, prj, pac, files):
3360     for file in files:
3361         u = makeurl(apiurl, ['source', prj, pac, file], query={'comment': 'removed %s' % (file, )})
3362         http_DELETE(u)
3363
3364 # old compat lib call
3365 def get_platforms(apiurl):
3366     return get_repositories(apiurl)
3367
3368 def get_repositories(apiurl):
3369     f = http_GET(makeurl(apiurl, ['platform']))
3370     tree = ET.parse(f)
3371     r = [ node.get('name') for node in tree.getroot() ]
3372     r.sort()
3373     return r
3374
3375
3376 # old compat lib call
3377</