SubversionClient.py 11.6 KB
Newer Older
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1 2 3 4
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
#                    Yoshinori Okuji <yo@nexedi.com>
Christophe Dumez's avatar
Christophe Dumez committed
5
#                    Christophe Dumez <christophe@nexedi.com>
Yoshinori Okuji's avatar
Yoshinori Okuji committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

from Acquisition import Implicit
31

Christophe Dumez's avatar
Christophe Dumez committed
32
import time, os
Yoshinori Okuji's avatar
Yoshinori Okuji committed
33 34 35 36
from Products.ERP5Type.Utils import convertToUpperCase
from MethodObject import Method
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
37
from Products.ERP5Type import Permissions
Christophe Dumez's avatar
Christophe Dumez committed
38
from Products.PythonScripts.Utility import allow_class
39
from tempfile import mktemp
40
from Products.ERP5.Document.BusinessTemplate import removeAll
Yoshinori Okuji's avatar
Yoshinori Okuji committed
41

42 43 44 45
class SubversionError(Exception):
  """The base exception class for the Subversion interface.
  """
  pass
46
  
47 48 49 50 51 52 53 54 55
class SubversionInstallationError(SubversionError):
  """Raised when an installation is broken.
  """
  pass
  
class SubversionTimeoutError(SubversionError):
  """Raised when a Subversion transaction is too long.
  """
  pass
Yoshinori Okuji's avatar
Yoshinori Okuji committed
56
    
57 58
try:
  import pysvn
Yoshinori Okuji's avatar
Yoshinori Okuji committed
59 60 61 62
  
  class SubversionLoginError(SubversionError):
    """Raised when an authentication is required.
    """
63 64
    # Declarative Security
    security = ClassSecurityInfo()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
65 66 67
    def __init__(self, realm = None):
      self._realm = realm
  
68
    security.declarePublic('getRealm')
Yoshinori Okuji's avatar
Yoshinori Okuji committed
69 70 71 72 73 74
    def getRealm(self):
      return self._realm
      
  class SubversionSSLTrustError(SubversionError):
    """Raised when a SSL certificate is not trusted.
    """
Christophe Dumez's avatar
Christophe Dumez committed
75 76 77
    # Declarative Security
    security = ClassSecurityInfo()
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
78 79
    def __init__(self, trust_dict = None):
      self._trust_dict = trust_dict
Christophe Dumez's avatar
Christophe Dumez committed
80 81
      
    security.declarePublic('getTrustDict')
Yoshinori Okuji's avatar
Yoshinori Okuji committed
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
    def getTrustDict(self):
      return self._trust_dict
  
  class Callback:
    """The base class for callback functions.
    """
    def __init__(self, client):
      self.client = client
  
    def __call__(self, *args):
      pass
  
  class CancelCallback(Callback):
    def __call__(self):
      current_time = time.time()
      if current_time - self.client.creation_time > self.client.getTimeout():
        raise SubversionTimeoutError, 'too long transaction'
        #return True
      return False
  
  class GetLogMessageCallback(Callback):
    def __call__(self):
      message = self.client.getLogMessage()
      if message:
        return True, message
      return False, ''
  
  class GetLoginCallback(Callback):
    def __call__(self, realm, username, may_save):
      user, password = self.client.getLogin(realm)
112
      if not username or not password:
Christophe Dumez's avatar
Christophe Dumez committed
113 114
        self.client.setException(SubversionLoginError(realm))
        return False, '', '', False
Yoshinori Okuji's avatar
Yoshinori Okuji committed
115 116 117 118 119 120 121 122 123 124 125
      return True, user, password, False
  
  class NotifyCallback(Callback):
    def __call__(self, event_dict):
      # FIXME: should accumulate information for the user
      pass
  
  class SSLServerTrustPromptCallback(Callback):
    def __call__(self, trust_dict):
      trust, permanent = self.client.trustSSLServer(trust_dict)
      if not trust:
126 127
        self.client.setException(SubversionSSLTrustError(trust_dict))
        return False, 0, False
Yoshinori Okuji's avatar
Yoshinori Okuji committed
128 129 130 131 132 133 134 135 136 137 138 139 140
      # XXX SSL server certificate failure bits are not defined in pysvn.
      # 0x8 means that the CA is unknown.
      return True, 0x8, permanent

  # Wrap objects defined in pysvn so that skins have access to attributes in the ERP5 way.
  class Getter(Method):
    def __init__(self, key):
      self._key = key
  
    def __call__(self, instance):
      value = getattr(instance._obj, self._key)
      if type(value) == type(u''):
        value = value.encode('utf-8')
141 142
      #elif isinstance(value, pysvn.Entry):
      elif str(type(value)) == "<type 'entry'>":
Yoshinori Okuji's avatar
Yoshinori Okuji committed
143
        value = Entry(value)
144 145
      #elif isinstance(value, pysvn.Revision):
      elif str(type(value)) == "<type 'revision'>":
Yoshinori Okuji's avatar
Yoshinori Okuji committed
146 147 148 149 150
        value = Revision(value)
      return value

  def initializeAccessors(klass):
    klass.security = ClassSecurityInfo()
151
    klass.security.declareObjectPublic()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
152 153 154 155
    for attr in klass.attribute_list:
      name = 'get' + convertToUpperCase(attr)
      setattr(klass, name, Getter(attr))
      klass.security.declarePublic(name)
156
    InitializeClass(klass)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
157 158 159 160 161 162 163 164

  class ObjectWrapper(Implicit):
    attribute_list = ()
    
    def __init__(self, obj):
      self._obj = obj
  
  class Status(ObjectWrapper):
165 166 167
    # XXX Big Hack to fix a bug
    __allow_access_to_unprotected_subobjects__ = 1
    attribute_list = ('path', 'entry', 'is_versioned', 'is_locked', 'is_copied', 'is_switched', 'prop_status', 'text_status', 'repos_prop_status', 'repos_text_status')
Yoshinori Okuji's avatar
Yoshinori Okuji committed
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
  initializeAccessors(Status)
  
  class Entry(ObjectWrapper):
    attribute_list = ('checksum', 'commit_author', 'commit_revision', 'commit_time',
                      'conflict_new', 'conflict_old', 'conflict_work', 'copy_from_revision',
                      'copy_from_url', 'is_absent', 'is_copied', 'is_deleted', 'is_valid',
                      'kind', 'name', 'properties_time', 'property_reject_file', 'repos',
                      'revision', 'schedule', 'text_time', 'url', 'uuid')

  class Revision(ObjectWrapper):
    attribute_list = ('kind', 'date', 'number')
  initializeAccessors(Revision)

  
  class SubversionClient(Implicit):
    """This class wraps pysvn's Client class.
    """
    log_message = None
    timeout = 60 * 5
    
188
    def __init__(self, container, **kw):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
189 190
      self.client = pysvn.Client()
      self.client.set_auth_cache(0)
191 192 193 194 195 196
      obj = self.__of__(container)
      self.client.callback_cancel = CancelCallback(obj)
      self.client.callback_get_log_message = GetLogMessageCallback(obj)
      self.client.callback_get_login = GetLoginCallback(obj)
      self.client.callback_notify = NotifyCallback(obj)
      self.client.callback_ssl_server_trust_prompt = SSLServerTrustPromptCallback(obj)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
197 198
      self.creation_time = time.time()
      self.__dict__.update(kw)
199
      self.exception = None
Yoshinori Okuji's avatar
Yoshinori Okuji committed
200 201 202

    def getLogMessage(self):
      return self.log_message
203
    
204 205
    def getLogin(self, realm):
      return self.aq_parent._getLogin(realm)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
206 207 208

    def getTimeout(self):
      return self.timeout
209
        
Yoshinori Okuji's avatar
Yoshinori Okuji committed
210 211
    def trustSSLServer(self, trust_dict):
      return self.aq_parent._trustSSLServer(trust_dict)
212 213 214 215 216 217

    def setException(self, exc):
      self.exception = exc

    def getException(self):
      return self.exception
218
    
219
    def checkin(self, path, log_message, recurse):
220
      try:
221
        return self.client.checkin(path, log_message=log_message or 'none', recurse=recurse)
222 223 224 225 226 227
      except pysvn.ClientError, error:
        excep = self.getException()
        if excep:
          raise excep
        else:
          raise error
Yoshinori Okuji's avatar
Yoshinori Okuji committed
228

229 230 231
    def export(self, src, dest):
        return self.client.update(src_url_or_path=src, dest_path=dest)
        
232 233 234 235 236 237 238 239 240 241
    def update(self, path):
      try:
        return self.client.update(path)
      except pysvn.ClientError, error:
        excep = self.getException()
        if excep:
          raise excep
        else:
          raise error
        
Yoshinori Okuji's avatar
Yoshinori Okuji committed
242 243 244
    def status(self, path, **kw):
      # Since plain Python classes are not convenient in Zope, convert the objects.
      return [Status(x) for x in self.client.status(path, **kw)]
Christophe Dumez's avatar
Christophe Dumez committed
245
    
246 247 248 249 250 251
    def removeAllInList(self, list):
      """Remove all files and folders in list
      """
      for file in list:
        removeAll(file)
      
252
    def diff(self, path, revision1, revision2):
253
      tmp = mktemp()
254
      os.makedirs(tmp)
255 256 257 258 259
      if not revision1 or not revision2:
        diff = self.client.diff(tmp_path=tmp, url_or_path=path, recurse=False)
      else:
        diff = self.client.diff(tmp_path=tmp, url_or_path=path, recurse=False, revision1=pysvn.Revision(pysvn.opt_revision_kind.number,revision1), revision2=pysvn.Revision(pysvn.opt_revision_kind.number,revision2))
      # clean up temp dir
260
      self.activate().removeAllInList([tmp,])
261
      return diff
262
    
263 264
    def revert(self, path, recurse=False):
      return self.client.revert(path, recurse)
Christophe Dumez's avatar
Christophe Dumez committed
265
    
266 267 268
    def switch(self, path, url):
      return self.client.switch(path=path, url=url)
    
Christophe Dumez's avatar
Christophe Dumez committed
269
    def log(self, path):
270 271 272
      try:
        log_list = self.client.log(path)
      except pysvn.ClientError, error:
273 274
        if 'path not found' in error.args[0]:
          return
275 276 277 278 279 280 281 282 283 284 285
        excep = self.getException()
        if excep:
          raise excep
        else:
          raise error
      # Edit list to make it more usable in zope
      for rev_dict in log_list:
        rev_dict['revision'] = rev_dict['revision'].number
        rev_dict['date'] = time.ctime(rev_dict['date'])
      return log_list
        
286
    def add(self, path):
287
      return self.client.add(path=path, force=True)
288

Christophe Dumez's avatar
Christophe Dumez committed
289 290 291
    def resolved(self, path):
      return self.client.resolved(path=path)
    
292
    def info(self, path):
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
      try:
        entry = self.client.info(path=path)
      except pysvn.ClientError, error:
        excep = self.getException()
        if excep:
          raise excep
        else:
          raise error
      # transform entry to dict to make it more usable in zope
      members_tuple=('url', 'uuid', 'revision', 'kind', 'commit_author', 'commit_revision', 'commit_time',)
      entry_dict = dict([(member,getattr(entry,member)) for member in members_tuple])
      entry_dict['revision'] = entry_dict['revision'].number
      entry_dict['commit_revision'] = entry_dict['commit_revision'].number
      entry_dict['commit_time'] = time.ctime(entry_dict['commit_time'])
      return entry_dict
      
Christophe Dumez's avatar
Christophe Dumez committed
309
    def ls(self, path):
310 311 312
      try:
        dict_list = self.client.ls(url_or_path=path, recurse=False)
      except pysvn.ClientError, error:
313 314
        if 'non-existent' in error.args[0]:
          return
315 316 317 318 319 320 321 322 323 324
        excep = self.getException()
        if excep:
          raise excep
        else:
          raise error
       #Modify the list to make it more usable in zope
      for dict in dict_list:
        dict['created_rev']=dict['created_rev'].number
        dict['time']=time.ctime(dict['time'])
      return dict_list
Christophe Dumez's avatar
Christophe Dumez committed
325

326 327 328
    def cleanup(self, path):
      return self.client.cleanup(path=path)

329
    def remove(self, path):
330
      return self.client.remove(url_or_path=path, force=True)
331

Yoshinori Okuji's avatar
Yoshinori Okuji committed
332
  def newSubversionClient(container, **kw):
333
    return SubversionClient(container, **kw).__of__(container)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
334
    
Christophe Dumez's avatar
Christophe Dumez committed
335 336
  InitializeClass(SubversionSSLTrustError)
  allow_class(SubversionSSLTrustError)
337 338
  InitializeClass(SubversionLoginError)
  allow_class(SubversionLoginError)
Christophe Dumez's avatar
Christophe Dumez committed
339
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
340
except ImportError:
341
  from zLOG import LOG, WARNING
Yoshinori Okuji's avatar
Yoshinori Okuji committed
342
  LOG('SubversionTool', WARNING,
343
      'could not import pysvn; until pysvn is installed properly, this tool will not work.')
Yoshinori Okuji's avatar
Yoshinori Okuji committed
344
  def newSubversionClient(container, **kw):
345
    raise SubversionInstallationError, 'pysvn library is not installed'