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