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