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