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