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