- fixes for old python versions
[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 is None or not r.isdigit():
1881         return None
1882     return int(r)
1883
1884 def read_inconflict(dir):
1885     r = []
1886     fname = os.path.join(dir, store, '_in_conflict')
1887
1888     if os.path.exists(fname):
1889         r = [ line.strip() for line in open(fname) ]
1890
1891     return r
1892
1893
1894 def parseargs(list_of_args):
1895     """Convenience method osc's commandline argument parsing.
1896
1897     If called with an empty tuple (or list), return a list containing the current directory.
1898     Otherwise, return a list of the arguments."""
1899     if list_of_args:
1900         return list(list_of_args)
1901     else:
1902         return [os.curdir]
1903
1904
1905 def statfrmt(statusletter, filename):
1906     return '%s    %s' % (statusletter, filename)
1907
1908
1909 def pathjoin(a, *p):
1910     """Join two or more pathname components, inserting '/' as needed. Cut leading ./"""
1911     path = os.path.join(a, *p)
1912     if path.startswith('./'):
1913         path = path[2:]
1914     return path
1915
1916
1917 def makeurl(baseurl, l, query=[]):
1918     """Given a list of path compoments, construct a complete URL.
1919
1920     Optional parameters for a query string can be given as a list, as a
1921     dictionary, or as an already assembled string.
1922     In case of a dictionary, the parameters will be urlencoded by this
1923     function. In case of a list not -- this is to be backwards compatible.
1924     """
1925
1926     if conf.config['verbose'] > 1:
1927         print 'makeurl:', baseurl, l, query
1928
1929     if type(query) == type(list()):
1930         query = '&'.join(query)
1931     elif type(query) == type(dict()):
1932         query = urlencode(query)
1933
1934     scheme, netloc = urlsplit(baseurl)[0:2]
1935     return urlunsplit((scheme, netloc, '/'.join(l), query, ''))
1936
1937
1938 def http_request(method, url, headers={}, data=None, file=None, timeout=100):
1939     """wrapper around urllib2.urlopen for error handling,
1940     and to support additional (PUT, DELETE) methods"""
1941
1942     filefd = None
1943
1944     if conf.config['http_debug']:
1945         print
1946         print
1947         print '--', method, url
1948
1949     if method == 'POST' and not file and not data:
1950         # adding data to an urllib2 request transforms it into a POST
1951         data = ''
1952
1953     req = urllib2.Request(url)
1954     api_host_options = {}
1955     try:
1956         api_host_options = conf.get_apiurl_api_host_options(url)
1957         for header, value in api_host_options['http_headers']:
1958             req.add_header(header, value)
1959     except:
1960         # "external" request (url is no apiurl)
1961         pass
1962
1963     req.get_method = lambda: method
1964
1965     # POST requests are application/x-www-form-urlencoded per default
1966     # since we change the request into PUT, we also need to adjust the content type header
1967     if method == 'PUT' or (method == 'POST' and data):
1968         req.add_header('Content-Type', 'application/octet-stream')
1969
1970     if type(headers) == type({}):
1971         for i in headers.keys():
1972             print headers[i]
1973             req.add_header(i, headers[i])
1974
1975     if file and not data:
1976         size = os.path.getsize(file)
1977         if size < 1024*512:
1978             data = open(file, 'rb').read()
1979         else:
1980             import mmap
1981             filefd = open(file, 'rb')
1982             try:
1983                 if sys.platform[:3] != 'win':
1984                     data = mmap.mmap(filefd.fileno(), os.path.getsize(file), mmap.MAP_SHARED, mmap.PROT_READ)
1985                 else:
1986                     data = mmap.mmap(filefd.fileno(), os.path.getsize(file))
1987                 data = buffer(data)
1988             except EnvironmentError, e:
1989                 if e.errno == 19:
1990                     sys.exit('\n\n%s\nThe file \'%s\' could not be memory mapped. It is ' \
1991                              '\non a filesystem which does not support this.' % (e, file))
1992                 elif hasattr(e, 'winerror') and e.winerror == 5:
1993                     # falling back to the default io
1994                     data = open(file, 'rb').read()
1995                 else:
1996                     raise
1997
1998     if conf.config['debug']: print method, url
1999
2000     old_timeout = socket.getdefaulttimeout()
2001     # XXX: dirty hack as timeout doesn't work with python-m2crypto
2002     if old_timeout != timeout and not api_host_options.get('sslcertck'):
2003         socket.setdefaulttimeout(timeout)
2004     try:
2005         fd = urllib2.urlopen(req, data=data)
2006     finally:
2007         if old_timeout != timeout and not api_host_options.get('sslcertck'):
2008             socket.setdefaulttimeout(old_timeout)
2009         if hasattr(conf.cookiejar, 'save'):
2010             conf.cookiejar.save(ignore_discard=True)
2011
2012     if filefd: filefd.close()
2013
2014     return fd
2015
2016
2017 def http_GET(*args, **kwargs):    return http_request('GET', *args, **kwargs)
2018 def http_POST(*args, **kwargs):   return http_request('POST', *args, **kwargs)
2019 def http_PUT(*args, **kwargs):    return http_request('PUT', *args, **kwargs)
2020 def http_DELETE(*args, **kwargs): return http_request('DELETE', *args, **kwargs)
2021
2022
2023 def init_project_dir(apiurl, dir, project):
2024     if not os.path.exists(dir):
2025         if conf.config['checkout_no_colon']:
2026             os.makedirs(dir)      # helpful with checkout_no_colon
2027         else:
2028             os.mkdir(dir)
2029     if not os.path.exists(os.path.join(dir, store)):
2030         os.mkdir(os.path.join(dir, store))
2031
2032     # print 'project=',project,'  dir=',dir
2033     store_write_project(dir, project)
2034     store_write_apiurl(dir, apiurl)
2035     if conf.config['do_package_tracking']:
2036         store_write_initial_packages(dir, project, [])
2037
2038 def init_package_dir(apiurl, project, package, dir, revision=None, files=True, limit_size=None):
2039     if not os.path.isdir(store):
2040         os.mkdir(store)
2041     os.chdir(store)
2042     f = open('_project', 'w')
2043     f.write(project + '\n')
2044     f.close
2045     f = open('_package', 'w')
2046     f.write(package + '\n')
2047     f.close
2048
2049     if limit_size:
2050         f = open('_size_limit', 'w')
2051         f.write(str(limit_size))
2052         f.close()
2053
2054     if files:
2055         f = open('_files', 'w')
2056         f.write(''.join(show_files_meta(apiurl, project, package, revision=revision, limit_size=limit_size)))
2057         f.close()
2058     else:
2059         # create dummy
2060         ET.ElementTree(element=ET.Element('directory')).write('_files')
2061
2062     f = open('_osclib_version', 'w')
2063     f.write(__store_version__ + '\n')
2064     f.close()
2065
2066     store_write_apiurl(os.path.pardir, apiurl)
2067
2068     os.chdir(os.pardir)
2069     return
2070
2071
2072 def check_store_version(dir):
2073     versionfile = os.path.join(dir, store, '_osclib_version')
2074     try:
2075         v = open(versionfile).read().strip()
2076     except:
2077         v = ''
2078
2079     if v == '':
2080         msg = 'Error: "%s" is not an osc package working copy.' % os.path.abspath(dir)
2081         if os.path.exists(os.path.join(dir, '.svn')):
2082             msg = msg + '\nTry svn instead of osc.'
2083         raise oscerr.NoWorkingCopy(msg)
2084
2085     if v != __store_version__:
2086         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']:
2087             # version is fine, no migration needed
2088             f = open(versionfile, 'w')
2089             f.write(__store_version__ + '\n')
2090             f.close()
2091             return
2092         msg = 'The osc metadata of your working copy "%s"' % dir
2093         msg += '\nhas __store_version__ = %s, but it should be %s' % (v, __store_version__)
2094         msg += '\nPlease do a fresh checkout or update your client. Sorry about the inconvenience.'
2095         raise oscerr.WorkingCopyWrongVersion, msg
2096
2097
2098 def meta_get_packagelist(apiurl, prj):
2099
2100     u = makeurl(apiurl, ['source', prj])
2101     f = http_GET(u)
2102     root = ET.parse(f).getroot()
2103     return [ node.get('name') for node in root.findall('entry') ]
2104
2105
2106 def meta_get_filelist(apiurl, prj, package, verbose=False, expand=False, revision=None):
2107     """return a list of file names,
2108     or a list File() instances if verbose=True"""
2109
2110     query = {}
2111     if expand:
2112         query['expand'] = 1
2113     if revision:
2114         query['rev'] = revision
2115     else:
2116         query['rev'] = 'latest'
2117
2118     u = makeurl(apiurl, ['source', prj, package], query=query)
2119     f = http_GET(u)
2120     root = ET.parse(f).getroot()
2121
2122     if not verbose:
2123         return [ node.get('name') for node in root.findall('entry') ]
2124
2125     else:
2126         l = []
2127         # rev = int(root.get('rev'))    # don't force int. also allow srcmd5 here.
2128         rev = root.get('rev')
2129         for node in root.findall('entry'):
2130             f = File(node.get('name'),
2131                      node.get('md5'),
2132                      int(node.get('size')),
2133                      int(node.get('mtime')))
2134             f.rev = rev
2135             l.append(f)
2136         return l
2137
2138
2139 def meta_get_project_list(apiurl):
2140     u = makeurl(apiurl, ['source'])
2141     f = http_GET(u)
2142     root = ET.parse(f).getroot()
2143     return sorted([ node.get('name') for node in root ])
2144
2145
2146 def show_project_meta(apiurl, prj):
2147     url = makeurl(apiurl, ['source', prj, '_meta'])
2148     f = http_GET(url)
2149     return f.readlines()
2150
2151
2152 def show_project_conf(apiurl, prj):
2153     url = makeurl(apiurl, ['source', prj, '_config'])
2154     f = http_GET(url)
2155     return f.readlines()
2156
2157
2158 def show_package_trigger_reason(apiurl, prj, pac, repo, arch):
2159     url = makeurl(apiurl, ['build', prj, repo, arch, pac, '_reason'])
2160     try:
2161         f = http_GET(url)
2162         return f.read()
2163     except urllib2.HTTPError, e:
2164         e.osc_msg = 'Error getting trigger reason for project \'%s\' package \'%s\'' % (prj, pac)
2165         raise
2166
2167
2168 def show_package_meta(apiurl, prj, pac):
2169     url = makeurl(apiurl, ['source', prj, pac, '_meta'])
2170     try:
2171         f = http_GET(url)
2172         return f.readlines()
2173     except urllib2.HTTPError, e:
2174         e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
2175         raise
2176
2177
2178 def show_attribute_meta(apiurl, prj, pac, subpac, attribute, with_defaults, with_project):
2179     path=[]
2180     path.append('source')
2181     path.append(prj)
2182     if pac:
2183         path.append(pac)
2184     if pac and subpac:
2185         path.append(subpac)
2186     path.append('_attribute')
2187     if attribute:
2188         path.append(attribute)
2189     query=[]
2190     if with_defaults:
2191         query.append("with_default=1")
2192     if with_project:
2193         query.append("with_project=1")
2194     url = makeurl(apiurl, path, query)
2195     try:
2196         f = http_GET(url)
2197         return f.readlines()
2198     except urllib2.HTTPError, e:
2199         e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
2200         raise
2201
2202
2203 def show_develproject(apiurl, prj, pac):
2204     m = show_package_meta(apiurl, prj, pac)
2205     try:
2206         return ET.fromstring(''.join(m)).find('devel').get('project')
2207     except:
2208         return None
2209
2210
2211 def show_pattern_metalist(apiurl, prj):
2212     url = makeurl(apiurl, ['source', prj, '_pattern'])
2213     try:
2214         f = http_GET(url)
2215         tree = ET.parse(f)
2216     except urllib2.HTTPError, e:
2217         e.osc_msg = 'show_pattern_metalist: Error getting pattern list for project \'%s\'' % prj
2218         raise
2219     r = [ node.get('name') for node in tree.getroot() ]
2220     r.sort()
2221     return r
2222
2223
2224 def show_pattern_meta(apiurl, prj, pattern):
2225     url = makeurl(apiurl, ['source', prj, '_pattern', pattern])
2226     try:
2227         f = http_GET(url)
2228         return f.readlines()
2229     except urllib2.HTTPError, e:
2230         e.osc_msg = 'show_pattern_meta: Error getting pattern \'%s\' for project \'%s\'' % (pattern, prj)
2231         raise
2232
2233
2234 class metafile:
2235     """metafile that can be manipulated and is stored back after manipulation."""
2236     def __init__(self, url, input, change_is_required=False, file_ext='.xml'):
2237         import tempfile
2238
2239         self.url = url
2240         self.change_is_required = change_is_required
2241         (fd, self.filename) = tempfile.mkstemp(prefix = 'osc_metafile.', suffix = file_ext)
2242         f = os.fdopen(fd, 'w')
2243         f.write(''.join(input))
2244         f.close()
2245         self.hash_orig = dgst(self.filename)
2246
2247     def sync(self):
2248         hash = dgst(self.filename)
2249         if self.change_is_required == True and hash == self.hash_orig:
2250             print 'File unchanged. Not saving.'
2251             os.unlink(self.filename)
2252             return
2253
2254         print 'Sending meta data...'
2255         # don't do any exception handling... it's up to the caller what to do in case
2256         # of an exception
2257         http_PUT(self.url, file=self.filename)
2258         os.unlink(self.filename)
2259         print 'Done.'
2260
2261     def edit(self):
2262         if sys.platform[:3] != 'win':
2263             editor = os.getenv('EDITOR', default='vim')
2264         else:
2265             editor = os.getenv('EDITOR', default='notepad')
2266         try:
2267             while 1:
2268                 subprocess.call('%s %s' % (editor, self.filename), shell=True)
2269                 try:
2270                     self.sync()
2271                     break
2272                 except urllib2.HTTPError, e:
2273                     error_help = "%d" % e.code
2274                     if e.headers.get('X-Opensuse-Errorcode'):
2275                         error_help = "%s (%d)" % (e.headers.get('X-Opensuse-Errorcode'), e.code)
2276
2277                     print >>sys.stderr, 'BuildService API error:', error_help
2278                     # examine the error - we can't raise an exception because we might want
2279                     # to try again
2280                     data = e.read()
2281                     if '<summary>' in data:
2282                         print >>sys.stderr, data.split('<summary>')[1].split('</summary>')[0]
2283                     input = raw_input('Try again? ([y/N]): ')
2284                     if input not in ['y', 'Y']:
2285                         break
2286         finally:
2287             self.discard()
2288
2289     def discard(self):
2290         if os.path.exists(self.filename):
2291             print 'discarding %s' % self.filename
2292             os.unlink(self.filename)
2293
2294
2295 # different types of metadata
2296 metatypes = { 'prj':     { 'path': 'source/%s/_meta',
2297                            'template': new_project_templ,
2298                            'file_ext': '.xml'
2299                          },
2300               'pkg':     { 'path'     : 'source/%s/%s/_meta',
2301                            'template': new_package_templ,
2302                            'file_ext': '.xml'
2303                          },
2304               'attribute':     { 'path'     : 'source/%s/%s/_meta',
2305                            'template': new_attribute_templ,
2306                            'file_ext': '.xml'
2307                          },
2308               'prjconf': { 'path': 'source/%s/_config',
2309                            'template': '',
2310                            'file_ext': '.txt'
2311                          },
2312               'user':    { 'path': 'person/%s',
2313                            'template': new_user_template,
2314                            'file_ext': '.xml'
2315                          },
2316               'pattern': { 'path': 'source/%s/_pattern/%s',
2317                            'template': new_pattern_template,
2318                            'file_ext': '.xml'
2319                          },
2320             }
2321
2322 def meta_exists(metatype,
2323                 path_args=None,
2324                 template_args=None,
2325                 create_new=True,
2326                 apiurl=None):
2327
2328     if not apiurl:
2329         apiurl = conf.config['apiurl']
2330     url = make_meta_url(metatype, path_args, apiurl)
2331     try:
2332         data = http_GET(url).readlines()
2333     except urllib2.HTTPError, e:
2334         if e.code == 404 and create_new:
2335             data = metatypes[metatype]['template']
2336             if template_args:
2337                 data = StringIO(data % template_args).readlines()
2338         else:
2339             raise e
2340     return data
2341
2342 def make_meta_url(metatype, path_args=None, apiurl=None):
2343     if not apiurl:
2344         apiurl = conf.config['apiurl']
2345     if metatype not in metatypes.keys():
2346         raise AttributeError('make_meta_url(): Unknown meta type \'%s\'' % metatype)
2347     path = metatypes[metatype]['path']
2348
2349     if path_args:
2350         path = path % path_args
2351
2352     return makeurl(apiurl, [path])
2353
2354
2355 def edit_meta(metatype,
2356               path_args=None,
2357               data=None,
2358               template_args=None,
2359               edit=False,
2360               change_is_required=False,
2361               apiurl=None):
2362
2363     if not apiurl:
2364         apiurl = conf.config['apiurl']
2365     if not data:
2366         data = meta_exists(metatype,
2367                            path_args,
2368                            template_args,
2369                            create_new = metatype != 'prjconf', # prjconf always exists, 404 => unknown prj
2370                            apiurl=apiurl)
2371
2372     if edit:
2373         change_is_required = True
2374
2375     url = make_meta_url(metatype, path_args, apiurl)
2376     f=metafile(url, data, change_is_required, metatypes[metatype]['file_ext'])
2377
2378     if edit:
2379         f.edit()
2380     else:
2381         f.sync()
2382
2383
2384 def show_files_meta(apiurl, prj, pac, revision=None, expand=False, linkrev=None, linkrepair=False, limit_size=None):
2385     query = {}
2386     if revision:
2387         query['rev'] = revision
2388     else:
2389         query['rev'] = 'latest'
2390     if linkrev:
2391         query['linkrev'] = linkrev
2392     elif conf.config['linkcontrol']:
2393         query['linkrev'] = 'base'
2394     if expand:
2395         query['expand'] = 1
2396     if linkrepair:
2397         query['emptylink'] = 1
2398     f = http_GET(makeurl(apiurl, ['source', prj, pac], query=query))
2399
2400     # look for "too large" files according to size limit and mark them
2401     root = ET.fromstring(''.join(f.readlines()))
2402     for e in root.findall('entry'):
2403         size = e.get('size')
2404         if size and limit_size and int(size) > int(limit_size):
2405              e.set('skipped', 'true')
2406     return ET.tostring(root)
2407
2408
2409 def show_upstream_srcmd5(apiurl, prj, pac, expand=False, revision=None):
2410     m = show_files_meta(apiurl, prj, pac, expand=expand, revision=revision)
2411     return ET.fromstring(''.join(m)).get('srcmd5')
2412
2413
2414 def show_upstream_xsrcmd5(apiurl, prj, pac, revision=None, linkrev=None, linkrepair=False):
2415     m = show_files_meta(apiurl, prj, pac, revision=revision, linkrev=linkrev, linkrepair=linkrepair)
2416     try:
2417         # only source link packages have a <linkinfo> element.
2418         li_node = ET.fromstring(''.join(m)).find('linkinfo')
2419     except:
2420         return None
2421
2422     li = Linkinfo()
2423     li.read(li_node)
2424
2425     if li.haserror():
2426         raise oscerr.LinkExpandError(prj, pac, li.error)
2427     return li.xsrcmd5
2428
2429
2430 def show_upstream_rev(apiurl, prj, pac):
2431     m = show_files_meta(apiurl, prj, pac)
2432     return ET.fromstring(''.join(m)).get('rev')
2433
2434
2435 def read_meta_from_spec(specfile, *args):
2436     import codecs, locale, re
2437     """
2438     Read tags and sections from spec file. To read out
2439     a tag the passed argument mustn't end with a colon. To
2440     read out a section the passed argument must start with
2441     a '%'.
2442     This method returns a dictionary which contains the
2443     requested data.
2444     """
2445
2446     if not os.path.isfile(specfile):
2447         raise IOError('\'%s\' is not a regular file' % specfile)
2448
2449     try:
2450         lines = codecs.open(specfile, 'r', locale.getpreferredencoding()).readlines()
2451     except UnicodeDecodeError:
2452         lines = open(specfile).readlines()
2453
2454     tags = []
2455     sections = []
2456     spec_data = {}
2457
2458     for itm in args:
2459         if itm.startswith('%'):
2460             sections.append(itm)
2461         else:
2462             tags.append(itm)
2463
2464     tag_pat = '(?P<tag>^%s)\s*:\s*(?P<val>.*)'
2465     for tag in tags:
2466         m = re.compile(tag_pat % tag, re.I | re.M).search(''.join(lines))
2467         if m and m.group('val'):
2468             spec_data[tag] = m.group('val').strip()
2469         else:
2470             print >>sys.stderr, 'error - tag \'%s\' does not exist' % tag
2471             sys.exit(1)
2472
2473     section_pat = '^%s\s*?$'
2474     for section in sections:
2475         m = re.compile(section_pat % section, re.I | re.M).search(''.join(lines))
2476         if m:
2477             start = lines.index(m.group()+'\n') + 1
2478         else:
2479             print >>sys.stderr, 'error - section \'%s\' does not exist' % section
2480             sys.exit(1)
2481         data = []
2482         for line in lines[start:]:
2483             if line.startswith('%'):
2484                 break
2485             data.append(line)
2486         spec_data[section] = data
2487
2488     return spec_data
2489
2490
2491 def edit_message(footer='', template=''):
2492     delim = '--This line, and those below, will be ignored--\n'
2493     import tempfile
2494     (fd, filename) = tempfile.mkstemp(prefix = 'osc-commitmsg', suffix = '.diff')
2495     f = os.fdopen(fd, 'w')
2496     if template != '':
2497         f.write(template)
2498     f.write('\n')
2499     f.write(delim)
2500     f.write('\n')
2501     f.write(footer)
2502     f.close()
2503
2504     if sys.platform[:3] != 'win':
2505         editor = os.getenv('EDITOR', default='vim')
2506     else:
2507         editor = os.getenv('EDITOR', default='notepad')
2508     try:
2509         while 1:
2510             subprocess.call('%s %s' % (editor, filename), shell=True)
2511             msg = open(filename).read().split(delim)[0].rstrip()
2512
2513             if len(msg):
2514                 break
2515             else:
2516                 input = raw_input('Log message not specified\n'
2517                                   'a)bort, c)ontinue, e)dit: ')
2518                 if input in 'aA':
2519                     raise oscerr.UserAbort()
2520                 elif input in 'cC':
2521                     break
2522                 elif input in 'eE':
2523                     pass
2524     finally:
2525         os.unlink(filename)
2526     return msg
2527
2528
2529 def create_delete_request(apiurl, project, package, message):
2530
2531     import cgi
2532
2533     if package:
2534         package = """package="%s" """ % (package)
2535     else:
2536         package = ""
2537
2538     xml = """\
2539 <request>
2540     <action type="delete">
2541         <target project="%s" %s/>
2542     </action>
2543     <state name="new"/>
2544     <description>%s</description>
2545 </request>
2546 """ % (project, package,
2547        cgi.escape(message or ''))
2548
2549     u = makeurl(apiurl, ['request'], query='cmd=create')
2550     f = http_POST(u, data=xml)
2551
2552     root = ET.parse(f).getroot()
2553     return root.get('id')
2554
2555
2556 def create_change_devel_request(apiurl,
2557                                 devel_project, devel_package,
2558                                 project, package,
2559                                 message):
2560
2561     import cgi
2562     xml = """\
2563 <request>
2564     <action type="change_devel">
2565         <source project="%s" package="%s" />
2566         <target project="%s" package="%s" />
2567     </action>
2568     <state name="new"/>
2569     <description>%s</description>
2570 </request>
2571 """ % (devel_project,
2572        devel_package,
2573        project,
2574        package,
2575        cgi.escape(message or ''))
2576
2577     u = makeurl(apiurl, ['request'], query='cmd=create')
2578     f = http_POST(u, data=xml)
2579
2580     root = ET.parse(f).getroot()
2581     return root.get('id')
2582
2583
2584 # This creates an old style submit request for server api 1.0
2585 def create_submit_request(apiurl,
2586                          src_project, src_package,
2587                          dst_project=None, dst_package=None,
2588                          message=None, orev=None, src_update=None):
2589
2590     import cgi
2591     options_block=""
2592     if src_update:
2593         options_block="""<options><sourceupdate>%s</sourceupdate></options> """ % (src_update)
2594
2595     # Yes, this kind of xml construction is horrible
2596     targetxml = ""
2597     if dst_project:
2598         packagexml = ""
2599         if dst_package:
2600             packagexml = """package="%s" """ %( dst_package )
2601         targetxml = """<target project="%s" %s /> """ %( dst_project, packagexml )
2602     # XXX: keep the old template for now in order to work with old obs instances
2603     xml = """\
2604 <request type="submit">
2605     <submit>
2606         <source project="%s" package="%s" rev="%s"/>
2607         %s
2608         %s
2609     </submit>
2610     <state name="new"/>
2611     <description>%s</description>
2612 </request>
2613 """ % (src_project,
2614        src_package,
2615        orev or show_upstream_rev(apiurl, src_project, src_package),
2616        targetxml,
2617        options_block,
2618        cgi.escape(message or ""))
2619
2620     u = makeurl(apiurl, ['request'], query='cmd=create')
2621     f = http_POST(u, data=xml)
2622
2623     root = ET.parse(f).getroot()
2624     return root.get('id')
2625
2626
2627 def get_request(apiurl, reqid):
2628     u = makeurl(apiurl, ['request', reqid])
2629     f = http_GET(u)
2630     root = ET.parse(f).getroot()
2631
2632     r = Request()
2633     r.read(root)
2634     return r
2635
2636
2637 def change_review_state(apiurl, reqid, newstate, by_user='', by_group='', message='', supersed=''):
2638     u = makeurl(apiurl,
2639                 ['request', reqid],
2640                 query={'cmd': 'changereviewstate', 'newstate': newstate, 'by_user': by_user, 'superseded_by': supersed})
2641     f = http_POST(u, data=message)
2642     return f.read()
2643
2644 def change_request_state(apiurl, reqid, newstate, message='', supersed=''):
2645     u = makeurl(apiurl,
2646                 ['request', reqid],
2647                 query={'cmd': 'changestate', 'newstate': newstate, 'superseded_by': supersed})
2648     f = http_POST(u, data=message)
2649     return f.read()
2650
2651
2652 def get_request_list(apiurl, project='', package='', req_who='', req_state=('new',), req_type=None, exclude_target_projects=[]):
2653     xpath = ''
2654     if not 'all' in req_state:
2655         for state in req_state:
2656             xpath = xpath_join(xpath, 'state/@name=\'%s\'' % state, inner=True)
2657     if req_who:
2658         xpath = xpath_join(xpath, '(state/@who=\'%(who)s\' or history/@who=\'%(who)s\')' % {'who': req_who}, op='and')
2659
2660     # XXX: we cannot use the '|' in the xpath expression because it is not supported
2661     #      in the backend
2662     todo = {}
2663     if project:
2664         todo['project'] = project
2665     if package:
2666         todo['package'] = package
2667     for kind, val in todo.iteritems():
2668         xpath = xpath_join(xpath, '(action/target/@%(kind)s=\'%(val)s\' or ' \
2669                                   'action/source/@%(kind)s=\'%(val)s\' or ' \
2670                                   'submit/target/@%(kind)s=\'%(val)s\' or ' \
2671                                   'submit/source/@%(kind)s=\'%(val)s\')' % {'kind': kind, 'val': val}, op='and')
2672     if req_type:
2673         xpath = xpath_join(xpath, 'action/@type=\'%s\'' % req_type, op='and')
2674     for i in exclude_target_projects:
2675         xpath = xpath_join(xpath, '(not(action/target/@project=\'%(prj)s\' or ' \
2676                                   'submit/target/@project=\'%(prj)s\'))' % {'prj': i}, op='and')
2677
2678     if conf.config['verbose'] > 1:
2679         print '[ %s ]' % xpath
2680     res = search(apiurl, request=xpath)
2681     collection = res['request']
2682     requests = []
2683     for root in collection.findall('request'):
2684         r = Request()
2685         r.read(root)
2686         requests.append(r)
2687     return requests
2688
2689 def get_user_projpkgs_request_list(apiurl, user, req_state=('new',), req_type=None, exclude_projects=[], projpkgs={}):
2690     """Return all new requests for all projects/packages where is user is involved"""
2691     if not projpkgs:
2692         res = get_user_projpkgs(apiurl, user, exclude_projects=exclude_projects)
2693         for i in res['project_id'].findall('project'):
2694             projpkgs[i.get('name')] = []
2695         for i in res['package_id'].findall('package'):
2696             if not i.get('project') in projpkgs.keys():
2697                 projpkgs.setdefault(i.get('project'), []).append(i.get('name'))
2698     xpath = ''
2699     for prj, pacs in projpkgs.iteritems():
2700         if not len(pacs):
2701             xpath = xpath_join(xpath, 'action/target/@project=\'%s\'' % prj, inner=True)
2702         else:
2703             xp = ''
2704             for p in pacs:
2705                 xp = xpath_join(xp, 'action/target/@package=\'%s\'' % p, inner=True)
2706             xp = xpath_join(xp, 'action/target/@project=\'%s\'' % prj, op='and')
2707             xpath = xpath_join(xpath, xp, inner=True)
2708     if req_type:
2709         xpath = xpath_join(xpath, 'action/@type=\'%s\'' % req_type, op='and')
2710     if not 'all' in req_state:
2711         xp = ''
2712         for state in req_state:
2713             xp = xpath_join(xp, 'state/@name=\'%s\'' % state, inner=True)
2714         xpath = xpath_join(xp, '(%s)' % xpath, op='and')
2715     res = search(apiurl, request=xpath)
2716     result = []
2717     for root in res['request'].findall('request'):
2718         r = Request()
2719         r.read(root)
2720         result.append(r)
2721     return result
2722
2723 def get_request_log(apiurl, reqid):
2724     r = get_request(conf.config['apiurl'], reqid)
2725     data = []
2726     frmt = '-' * 76 + '\n%s | %s | %s\n\n%s'
2727     # the description of the request is used for the initial log entry
2728     # otherwise its comment attribute would contain None
2729     if len(r.statehistory) >= 1:
2730         r.statehistory[-1].comment = r.descr
2731     else:
2732         r.state.comment = r.descr
2733     for state in [ r.state ] + r.statehistory:
2734         s = frmt % (state.name, state.who, state.when, str(state.comment))
2735         data.append(s)
2736     return data
2737
2738
2739 def get_user_meta(apiurl, user):
2740     u = makeurl(apiurl, ['person', quote_plus(user)])
2741     try:
2742         f = http_GET(u)
2743         return ''.join(f.readlines())
2744     except urllib2.HTTPError:
2745         print 'user \'%s\' not found' % user
2746         return None
2747
2748
2749 def get_user_data(apiurl, user, *tags):
2750     """get specified tags from the user meta"""
2751     meta = get_user_meta(apiurl, user)
2752     data = []
2753     if meta != None:
2754         root = ET.fromstring(meta)
2755         for tag in tags:
2756             try:
2757                 if root.find(tag).text != None:
2758                     data.append(root.find(tag).text)
2759                 else:
2760                     # tag is empty
2761                     data.append('-')
2762             except AttributeError:
2763                 # this part is reached if the tags tuple contains an invalid tag
2764                 print 'The xml file for user \'%s\' seems to be broken' % user
2765                 return []
2766     return data
2767
2768
2769 def get_source_file(apiurl, prj, package, filename, targetfilename=None, revision=None, progress_obj=None):
2770     import tempfile, shutil
2771     query = None
2772     if revision:
2773         query = { 'rev': revision }
2774     o = None
2775     try:
2776         try:
2777             (fd, tmpfile) = tempfile.mkstemp(prefix = filename, suffix = '.osc')
2778             o = os.fdopen(fd, 'wb')
2779             u = makeurl(apiurl, ['source', prj, package, pathname2url(filename)], query=query)
2780             for buf in streamfile(u, http_GET, BUFSIZE, progress_obj=progress_obj):
2781                 o.write(buf)
2782             o.close()
2783             shutil.move(tmpfile, targetfilename or filename)
2784             os.chmod(targetfilename or filename, 0644)
2785         except:
2786             os.unlink(tmpfile)
2787             raise
2788     finally:
2789         if o is not None:
2790             o.close()
2791
2792 def get_binary_file(apiurl, prj, repo, arch,
2793                     filename,
2794                     package = None,
2795                     target_filename = None,
2796                     target_mtime = None,
2797                     progress_meter = False):
2798
2799     target_filename = target_filename or filename
2800
2801     where = package or '_repository'
2802     u = makeurl(apiurl, ['build', prj, repo, arch, where, filename])
2803
2804     if progress_meter:
2805         sys.stdout.write("Downloading %s [  0%%]" % filename)
2806         sys.stdout.flush()
2807
2808     f = http_GET(u)
2809     binsize = int(f.headers['content-length'])
2810
2811     import tempfile
2812     (fd, tmpfilename) = tempfile.mkstemp(prefix = filename + '.', suffix = '.osc')
2813     os.chmod(tmpfilename, 0644)
2814
2815     try:
2816         o = os.fdopen(fd, 'wb')
2817
2818         downloaded = 0
2819         while 1:
2820             #buf = f.read(BUFSIZE)
2821             buf = f.read(16384)
2822             if not buf: break
2823             o.write(buf)
2824             downloaded += len(buf)
2825             if progress_meter:
2826                 completion = str(int((float(downloaded)/binsize)*100))
2827                 sys.stdout.write('%s%*s%%]' % ('\b'*5, 3, completion))
2828                 sys.stdout.flush()
2829         o.close()
2830
2831         if progress_meter:
2832             sys.stdout.write('\n')
2833
2834         shutil.move(tmpfilename, target_filename)
2835         if target_mtime:
2836             os.utime(target_filename, (-1, target_mtime))
2837
2838     # make sure that the temp file is cleaned up when we are interrupted
2839     finally:
2840         try: os.unlink(tmpfilename)
2841         except: pass
2842
2843 def dgst_from_string(str):
2844     # Python 2.5 depracates the md5 modules
2845     # Python 2.4 doesn't have hashlib yet
2846     try:
2847         import hashlib
2848         md5_hash = hashlib.md5()
2849     except ImportError:
2850         import md5
2851         md5_hash = md5.new()
2852     md5_hash.update(str)
2853     return md5_hash.hexdigest()
2854
2855 def dgst(file):
2856
2857     #if not os.path.exists(file):
2858         #return None
2859
2860     try:
2861         import hashlib
2862         md5 = hashlib
2863     except ImportError:
2864         import md5
2865         md5 = md5
2866     s = md5.md5()
2867     f = open(file, 'rb')
2868     while 1:
2869         buf = f.read(BUFSIZE)
2870         if not buf: break
2871         s.update(buf)
2872     return s.hexdigest()
2873     f.close()
2874
2875
2876 def binary(s):
2877     """return true if a string is binary data using diff's heuristic"""
2878     if s and '\0' in s[:4096]:
2879         return True
2880     return False
2881
2882
2883 def binary_file(fn):
2884     """read 4096 bytes from a file named fn, and call binary() on the data"""
2885     return binary(open(fn, 'rb').read(4096))
2886
2887
2888 def get_source_file_diff(dir, filename, rev, oldfilename = None, olddir = None, origfilename = None):
2889     """
2890     This methods diffs oldfilename against filename (so filename will
2891     be shown as the new file).
2892     The variable origfilename is used if filename and oldfilename differ
2893     in their names (for instance if a tempfile is used for filename etc.)
2894     """
2895
2896     import difflib
2897
2898     if not oldfilename:
2899         oldfilename = filename
2900
2901     if not olddir:
2902         olddir = os.path.join(dir, store)
2903
2904     if not origfilename:
2905         origfilename = filename
2906
2907     file1 = os.path.join(olddir, oldfilename)   # old/stored original
2908     file2 = os.path.join(dir, filename)         # working copy
2909
2910     f1 = open(file1, 'rb')
2911     s1 = f1.read()
2912     f1.close()
2913
2914     f2 = open(file2, 'rb')
2915     s2 = f2.read()
2916     f2.close()
2917
2918     if binary(s1) or binary (s2):
2919         d = ['Binary file %s has changed\n' % origfilename]
2920
2921     else:
2922         d = difflib.unified_diff(\
2923             s1.splitlines(1), \
2924             s2.splitlines(1), \
2925             fromfile = '%s\t(revision %s)' % (origfilename, rev), \
2926             tofile = '%s\t(working copy)' % origfilename)
2927
2928         # if file doesn't end with newline, we need to append one in the diff result
2929         d = list(d)
2930         for i, line in enumerate(d):
2931             if not line.endswith('\n'):
2932                 d[i] += '\n\\ No newline at end of file'
2933                 if i+1 != len(d):
2934                     d[i] += '\n'
2935
2936     return ''.join(d)
2937
2938 def make_diff(wc, revision):
2939     import tempfile
2940     changed_files = []
2941     added_files = []
2942     removed_files = []
2943     cmp_pac = None
2944     diff_hdr = 'Index: %s\n'
2945     diff_hdr += '===================================================================\n'
2946     diff = []
2947     olddir = os.getcwd()
2948     if not revision:
2949         # normal diff
2950         if wc.todo:
2951             for file in wc.todo:
2952                 if file in wc.skipped:
2953                     continue
2954                 if file in wc.filenamelist+wc.filenamelist_unvers:
2955                     state = wc.status(file)
2956                     if state == 'A':
2957                         added_files.append(file)
2958                     elif state == 'D':
2959                         removed_files.append(file)
2960                     elif state == 'M' or state == 'C':
2961                         changed_files.append(file)
2962                 else:
2963                     diff.append('osc: \'%s\' is not under version control' % file)
2964         else:
2965             for file in wc.filenamelist+wc.filenamelist_unvers:
2966                 if file in wc.skipped:
2967                     continue
2968                 state = wc.status(file)
2969                 if state == 'M' or state == 'C':
2970                     changed_files.append(file)
2971                 elif state == 'A':
2972                     added_files.append(file)
2973                 elif state == 'D':
2974                     removed_files.append(file)
2975     else:
2976         tmpdir  = tempfile.mkdtemp(str(revision), wc.name)
2977         os.chdir(tmpdir)
2978         init_package_dir(wc.apiurl, wc.prjname, wc.name, tmpdir, revision)
2979         cmp_pac = Package(tmpdir)
2980         if wc.todo:
2981             for file in wc.todo:
2982                 if file in cmp_pac.skipped:
2983                     continue
2984                 if file in cmp_pac.filenamelist:
2985                     if file in wc.filenamelist:
2986                         changed_files.append(file)
2987                     else:
2988                         diff.append('osc: \'%s\' is not under version control' % file)
2989                 else:
2990                     diff.append('osc: unable to find \'%s\' in revision %s' % (file, cmp_pac.rev))
2991         else:
2992             changed_files, added_files, removed_files = wc.comparePac(cmp_pac)
2993
2994     for file in changed_files:
2995         diff.append(diff_hdr % file)
2996         if cmp_pac == None:
2997             diff.append(get_source_file_diff(wc.absdir, file, wc.rev))
2998         else:
2999             cmp_pac.updatefile(file, revision)
3000             diff.append(get_source_file_diff(wc.absdir, file, revision, file,
3001                                              cmp_pac.absdir, file))
3002     (fd, tmpfile) = tempfile.mkstemp()
3003     for file in added_files:
3004         diff.append(diff_hdr % file)
3005         if cmp_pac == None:
3006             diff.append(get_source_file_diff(wc.absdir, file, wc.rev, os.path.basename(tmpfile),
3007                                              os.path.dirname(tmpfile), file))
3008         else:
3009             diff.append(get_source_file_diff(wc.absdir, file, revision, os.path.basename(tmpfile),
3010                                              os.path.dirname(tmpfile), file))
3011
3012     # FIXME: this is ugly but it cannot be avoided atm
3013     #        if a file is deleted via "osc rm file" we should keep the storefile.
3014     tmp_pac = None
3015     if cmp_pac == None and removed_files:
3016         tmpdir = tempfile.mkdtemp()
3017         os.chdir(tmpdir)
3018         init_package_dir(wc.apiurl, wc.prjname, wc.name, tmpdir, wc.rev)
3019         tmp_pac = Package(tmpdir)
3020         os.chdir(olddir)
3021
3022     for file in removed_files:
3023         diff.append(diff_hdr % file)
3024         if cmp_pac == None:
3025             tmp_pac.updatefile(file, tmp_pac.rev)
3026             diff.append(get_source_file_diff(os.path.dirname(tmpfile), os.path.basename(tmpfile),
3027                                              wc.rev, file, tmp_pac.storedir, file))
3028         else:
3029             cmp_pac.updatefile(file, revision)
3030             diff.append(get_source_file_diff(os.path.dirname(tmpfile), os.path.basename(tmpfile),
3031                                              revision, file, cmp_pac.storedir, file))
3032
3033     os.chdir(olddir)
3034     if cmp_pac != None:
3035         delete_dir(cmp_pac.absdir)
3036     if tmp_pac != None:
3037         delete_dir(tmp_pac.absdir)
3038     return diff
3039
3040
3041 def server_diff(apiurl,
3042                 old_project, old_package, old_revision,
3043                 new_project, new_package, new_revision, unified=False, missingok=False):
3044     query = {'cmd': 'diff', 'expand': '1'}
3045     if old_project:
3046         query['oproject'] = old_project
3047     if old_package:
3048         query['opackage'] = old_package
3049     if old_revision:
3050         query['orev'] = old_revision
3051     if new_revision:
3052         query['rev'] = new_revision
3053     if unified:
3054         query['unified'] = 1
3055     if missingok:
3056         query['missingok'] = 1
3057
3058     u = makeurl(apiurl, ['source', new_project, new_package], query=query)
3059
3060     f = http_POST(u)
3061     return f.read()
3062
3063
3064 def make_dir(apiurl, project, package, pathname=None, prj_dir=None):
3065     """
3066     creates the plain directory structure for a package dir.
3067     The 'apiurl' parameter is needed for the project dir initialization.
3068     The 'project' and 'package' parameters specify the name of the
3069     project and the package. The optional 'pathname' parameter is used
3070     for printing out the message that a new dir was created (default: 'prj_dir/package').
3071     The optional 'prj_dir' parameter specifies the path to the project dir (default: 'project').
3072     """
3073     prj_dir = prj_dir or project
3074
3075     # FIXME: carefully test each patch component of prj_dir,
3076     # if we have a .osc/_files entry at that level.
3077     #   -> if so, we have a package/project clash,
3078     #      and should rename this path component by appending '.proj'
3079     #      and give user a warning message, to discourage such clashes
3080
3081     pathname = pathname or getTransActPath(os.path.join(prj_dir, package))
3082     if is_package_dir(prj_dir):
3083         # we want this to become a project directory,
3084         # but it already is a package directory.
3085         raise oscerr.OscIOError(None, 'checkout_package: package/project clash. Moving myself away not implemented')
3086
3087     if not is_project_dir(prj_dir):
3088         # this directory could exist as a parent direory for one of our earlier
3089         # checked out sub-projects. in this case, we still need to initialize it.
3090         print statfrmt('A', prj_dir)
3091         init_project_dir(apiurl, prj_dir, project)
3092
3093     if is_project_dir(os.path.join(prj_dir, package)):
3094         # the thing exists, but is a project directory and not a package directory
3095         # FIXME: this should be a warning message to discourage package/project clashes
3096         raise oscerr.OscIOError(None, 'checkout_package: package/project clash. Moving project away not implemented')
3097
3098     if not os.path.exists(os.path.join(prj_dir, package)):
3099         print statfrmt('A', pathname)
3100         os.mkdir(os.path.join(prj_dir, package))
3101         os.mkdir(os.path.join(prj_dir, package, store))
3102
3103     return(os.path.join(prj_dir, package))
3104
3105
3106 def checkout_package(apiurl, project, package,
3107                      revision=None, pathname=None, prj_obj=None,
3108                      expand_link=False, prj_dir=None, service_files=None, progress_obj=None, limit_size=None):
3109     try:
3110         # the project we're in might be deleted.
3111         # that'll throw an error then.
3112         olddir = os.getcwd()
3113     except:
3114         olddir = os.environ.get("PWD")
3115
3116     if not prj_dir:
3117         prj_dir = olddir
3118     else:
3119         if sys.platform[:3] == 'win':
3120             prj_dir = prj_dir[:2] + prj_dir[2:].replace(':', ';')
3121         else:
3122             if conf.config['checkout_no_colon']:
3123                 prj_dir = prj_dir.replace(':', '/')
3124
3125     if not pathname:
3126         pathname = getTransActPath(os.path.join(prj_dir, package))
3127
3128     # before we create directories and stuff, check if the package actually
3129     # exists
3130     show_package_meta(apiurl, project, package)
3131
3132     isfrozen = 0
3133     if expand_link:
3134         # try to read from the linkinfo
3135         # if it is a link we use the xsrcmd5 as the revision to be
3136         # checked out
3137         try:
3138             x = show_upstream_xsrcmd5(apiurl, project, package, revision=revision)
3139         except:
3140             x = show_upstream_xsrcmd5(apiurl, project, package, revision=revision, linkrev='base')
3141             if x:
3142                 isfrozen = 1
3143         if x:
3144             revision = x
3145     os.chdir(make_dir(apiurl, project, package, pathname, prj_dir))
3146     init_package_dir(apiurl, project, package, store, revision, limit_size=limit_size)
3147     os.chdir(os.pardir)
3148     p = Package(package, progress_obj=progress_obj)
3149     if isfrozen:
3150         p.mark_frozen()
3151     for filename in p.filenamelist:
3152         if filename in p.skipped:
3153             continue
3154         if service_files or not filename.startswith('_service:'):
3155             p.updatefile(filename, revision)
3156             # print 'A   ', os.path.join(project, package, filename)
3157             print statfrmt('A', os.path.join(pathname, filename))
3158     if conf.config['do_package_tracking']:
3159         # check if we can re-use an existing project object
3160         if prj_obj == None:
3161             prj_obj = Project(os.getcwd())
3162         prj_obj.set_state(p.name, ' ')
3163         prj_obj.write_packages()
3164     os.chdir(olddir)
3165
3166
3167 def replace_pkg_meta(pkgmeta, new_name, new_prj, keep_maintainers = False,
3168                      dst_userid = None, keep_develproject = False):
3169     """
3170     update pkgmeta with new new_name and new_prj and set calling user as the
3171     only maintainer (unless keep_maintainers is set). Additionally remove the
3172     develproject entry (<devel />) unless keep_develproject is true.
3173     """
3174     root = ET.fromstring(''.join(pkgmeta))
3175     root.set('name', new_name)
3176     root.set('project', new_prj)
3177     if not keep_maintainers:
3178         for person in root.findall('person'):
3179             root.remove(person)
3180     if not keep_develproject:
3181         for dp in root.findall('devel'):
3182             root.remove(dp)
3183     return ET.tostring(root)
3184
3185 def link_to_branch(apiurl, project,  package):
3186     """
3187      convert a package with a _link + project.diff to a branch
3188     """
3189
3190     if '_link' in meta_get_filelist(apiurl, project, package):
3191         u = makeurl(apiurl, ['source', project, package], 'cmd=linktobranch')
3192         http_POST(u)
3193     else:
3194         raise oscerr.OscIOError(None, 'no _link file inside project \'%s\' package \'%s\'' % (project, package))
3195
3196 def link_pac(src_project, src_package, dst_project, dst_package, force, rev='', cicount='', disable_publish = False):
3197     """
3198     create a linked package
3199      - "src" is the original package
3200      - "dst" is the "link" package that we are creating here
3201     """
3202     meta_change = False
3203     dst_meta = ''
3204     try:
3205         dst_meta = meta_exists(metatype='pkg',
3206                                path_args=(quote_plus(dst_project), quote_plus(dst_package)),
3207                                template_args=None,
3208                                create_new=False, apiurl=conf.config['apiurl'])
3209     except:
3210         src_meta = show_package_meta(conf.config['apiurl'], src_project, src_package)
3211         dst_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
3212         meta_change = True
3213
3214     if disable_publish:
3215         meta_change = True
3216         root = ET.fromstring(''.join(dst_meta))
3217         elm = root.find('publish')
3218         if not elm:
3219             elm = ET.SubElement(root, 'publish')
3220         elm.clear()
3221         ET.SubElement(elm, 'disable')
3222         dst_meta = ET.tostring(root)
3223     if meta_change:
3224         edit_meta('pkg',
3225                   path_args=(dst_project, dst_package),
3226                   data=dst_meta)
3227     # create the _link file
3228     # but first, make sure not to overwrite an existing one
3229     if '_link' in meta_get_filelist(conf.config['apiurl'], dst_project, dst_package):
3230         if force:
3231             print >>sys.stderr, 'forced overwrite of existing _link file'
3232         else:
3233             print >>sys.stderr
3234             print >>sys.stderr, '_link file already exists...! Aborting'
3235             sys.exit(1)
3236
3237     if rev:
3238         rev = 'rev="%s"' % rev
3239     else:
3240         rev = ''
3241
3242     if cicount:
3243         cicount = 'cicount="%s"' % cicount
3244     else:
3245         cicount = ''
3246
3247     print 'Creating _link...',
3248     link_template = """\
3249 <link project="%s" package="%s" %s %s>
3250 <patches>
3251   <!-- <apply name="patch" /> apply a patch on the source directory  -->
3252   <!-- <topadd>%%define build_with_feature_x 1</topadd> add a line on the top (spec file only) -->
3253   <!-- <add>file.patch</add> add a patch to be applied after %%setup (spec file only) -->
3254   <!-- <delete>filename</delete> delete a file -->
3255 </patches>
3256 </link>
3257 """ % (src_project, src_package, rev, cicount)
3258
3259     u = makeurl(conf.config['apiurl'], ['source', dst_project, dst_package, '_link'])
3260     http_PUT(u, data=link_template)
3261     print 'Done.'
3262
3263 def aggregate_pac(src_project, src_package, dst_project, dst_package, repo_map = {}, disable_publish = False):
3264     """
3265     aggregate package
3266      - "src" is the original package
3267      - "dst" is the "aggregate" package that we are creating here
3268      - "map" is a dictionary SRC => TARGET repository mappings
3269     """
3270     meta_change = False
3271     dst_meta = ''
3272     try:
3273         dst_meta = meta_exists(metatype='pkg',
3274                                path_args=(quote_plus(dst_project), quote_plus(dst_package)),
3275                                template_args=None,
3276                                create_new=False, apiurl=conf.config['apiurl'])
3277     except:
3278         src_meta = show_package_meta(conf.config['apiurl'], src_project, src_package)
3279         dst_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
3280         meta_change = True
3281
3282     if disable_publish:
3283         meta_change = True
3284         root = ET.fromstring(''.join(dst_meta))
3285         elm = root.find('publish')
3286         if not elm:
3287             elm = ET.SubElement(root, 'publish')
3288         elm.clear()
3289         ET.SubElement(elm, 'disable')
3290         dst_meta = ET.tostring(root)
3291     if meta_change:
3292         edit_meta('pkg',
3293                   path_args=(dst_project, dst_package),
3294                   data=dst_meta)
3295
3296     # create the _aggregate file
3297     # but first, make sure not to overwrite an existing one
3298     if '_aggregate' in meta_get_filelist(conf.config['apiurl'], dst_project, dst_package):
3299         print >>sys.stderr
3300         print >>sys.stderr, '_aggregate file already exists...! Aborting'
3301         sys.exit(1)
3302
3303     print 'Creating _aggregate...',
3304     aggregate_template = """\
3305 <aggregatelist>
3306   <aggregate project="%s">
3307 """ % (src_project)
3308     for tgt, src in repo_map.iteritems():
3309         aggregate_template += """\
3310     <repository target="%s" source="%s" />
3311 """ % (tgt, src)
3312
3313     aggregate_template += """\
3314     <package>%s</package>
3315   </aggregate>
3316 </aggregatelist>
3317 """ % ( src_package)
3318
3319     u = makeurl(conf.config['apiurl'], ['source', dst_project, dst_package, '_aggregate'])
3320     http_PUT(u, data=aggregate_template)
3321     print 'Done.'
3322
3323
3324 def attribute_branch_pkg(apiurl, attribute, maintained_update_project_attribute, package, targetproject, return_existing=False):
3325     """
3326     Branch packages defined via attributes (via API call)
3327     """
3328     query = { 'cmd': 'branch' }
3329     query['attribute'] = attribute
3330     if targetproject:
3331         query['target_project'] = targetproject
3332     if package:
3333         query['package'] = package
3334     if maintained_update_project_attribute:
3335         query['update_project_attribute'] = maintained_update_project_attribute
3336
3337     u = makeurl(apiurl, ['source'], query=query)
3338     f = None
3339     try:
3340         f = http_POST(u)
3341     except urllib2.HTTPError, e:
3342         msg = ''.join(e.readlines())
3343         msg = msg.split('<summary>')[1]
3344         msg = msg.split('</summary>')[0]
3345         raise oscerr.APIError(msg)
3346
3347     r = f.read()
3348     r = r.split('targetproject">')[1]
3349     r = r.split('</data>')[0]
3350     return r
3351
3352
3353 def branch_pkg(apiurl, src_project, src_package, nodevelproject=False, rev=None, target_project=None, target_package=None, return_existing=False, msg=''):
3354     """
3355     Branch a package (via API call)
3356     """
3357     query = { 'cmd': 'branch' }
3358     if nodevelproject:
3359         query['ignoredevel'] = '1'
3360     if rev:
3361         query['rev'] = rev
3362     if target_project:
3363         query['target_project'] = target_project
3364     if target_package:
3365         query['target_package'] = target_package
3366     if msg:
3367         query['comment'] = msg
3368     u = makeurl(apiurl, ['source', src_project, src_package], query=query)
3369     try: