SubversionTool.py 29.5 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 31 32 33 34 35 36
#
# 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 Products.CMFCore.utils import UniqueObject
from AccessControl import ClassSecurityInfo
from Globals import InitializeClass, DTMLFile
from Products.ERP5Type.Document.Folder import Folder
from Products.ERP5Type import Permissions
from Products.ERP5Subversion import _dtmldir
from Products.ERP5Subversion.SubversionClient import newSubversionClient
37
import os, re, commands, time, exceptions
Yoshinori Okuji's avatar
Yoshinori Okuji committed
38 39 40 41
from DateTime import DateTime
from cPickle import dumps, loads
from App.config import getConfiguration
from zExceptions import Unauthorized
Christophe Dumez's avatar
Christophe Dumez committed
42 43
from OFS.Image import manage_addFile
from cStringIO import StringIO
44
from tempfile import mktemp
45
from shutil import copy
Aurel's avatar
Aurel committed
46 47 48 49

try:
  from base64 import b64encode, b64decode
except ImportError:
50
  from base64 import encodestring as b64encode, decodestring as b64decode
51 52 53 54

class Error(exceptions.EnvironmentError):
    pass

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
def removeAll(entry):
  '''
    Remove all files and directories under 'entry'.
    XXX: This is defined here, because os.removedirs() is buggy.
  '''
  try:
    if os.path.isdir(entry) and not os.path.islink(entry):
      pwd = os.getcwd()
      os.chmod(entry, 0755)
      os.chdir(entry)
      for e in os.listdir(os.curdir):
        removeAll(e)
      os.chdir(pwd)
      os.rmdir(entry)
    else:
      if not os.path.islink(entry):
        os.chmod(entry, 0644)
      os.remove(entry)
  except OSError:
    pass
75 76
      
def copytree(src, dst, symlinks=False):
77
    """Recursively copy a directory tree using copy().
78 79

    If exception(s) occur, an Error is raised with a list of reasons.
80
    dst dir must exist
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96

    If the optional symlinks flag is true, symbolic links in the
    source tree result in symbolic links in the destination tree; if
    it is false, the contents of the files pointed to by symbolic
    links are copied.
    """
    names = os.listdir(src)
    errors = []
    for name in names:
        srcname = os.path.join(src, name)
        dstname = os.path.join(dst, name)
        try:
            if symlinks and os.path.islink(srcname):
                linkto = os.readlink(srcname)
                os.symlink(linkto, dstname)
            elif os.path.isdir(srcname):
97 98
                if not os.path.exists(dstname):
                  os.makedirs(dstname)
99 100
                copytree(srcname, dstname, symlinks)
            else:
101
                copy(srcname, dstname)
102 103 104 105
        except (IOError, os.error), why:
            errors.append((srcname, dstname, why))
    if errors:
        raise Error, errors
106 107

  
108 109
class File :
  # Constructor
110 111 112
  def __init__(self, full_path, msg_status) :
    self.full_path = full_path
    self.msg_status = msg_status
113
    self.name = full_path.split(os.sep)[-1]
114 115 116 117
## End of File Class

class Dir :
  # Constructor
118 119 120
  def __init__(self, full_path, msg_status) :
    self.full_path = full_path
    self.msg_status = msg_status
121
    self.name = full_path.split(os.sep)[-1]
122
    self.sub_dirs = [] # list of sub directories
123 124 125

  # return a list of sub directories' names
  def getSubDirs(self) :
126
    return [d.name for d in self.sub_dirs]
127 128

  # return directory in subdirs given its name
129
  def getDir(self, name):
130
    for d in self.sub_dirs:
131
      if d.name == name:
132 133
        return d
## End of Dir Class
134 135 136 137 138 139 140 141

class DiffFile:
  # Members :
  # - path : path of the modified file
  # - children : sub codes modified
  # - old_revision
  # - new_revision

142
  def __init__(self, raw_diff):
143 144 145 146 147
    if '@@' not in raw_diff:
      self.binary=True
      return
    else:
      self.binary=False
148
    self.header = raw_diff.split('@@')[0][:-1]
149
    # Getting file path in header
150
    self.path = self.header.split('====')[0][:-1].strip()
151
    # Getting revisions in header
152
    for line in self.header.split(os.linesep):
153
      if line.startswith('--- '):
154
        tmp = re.search('\\([^)]+\\)$', line)
155
        self.old_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip()
156
      if line.startswith('+++ '):
157
        tmp = re.search('\\([^)]+\\)$', line)
158
        self.new_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip()
159
    # Splitting the body from the header
160
    self.body = os.linesep.join(raw_diff.strip().split(os.linesep)[4:])
161
    # Now splitting modifications
162
    self.children = []
163 164
    first = True
    tmp = []
165
    for line in self.body.split(os.linesep):
166 167
      if line:
        if line.startswith('@@') and not first:
168
          self.children.append(CodeBlock(os.linesep.join(tmp)))
169 170 171 172
          tmp = [line,]
        else:
          first = False
          tmp.append(line)
173
    self.children.append(CodeBlock(os.linesep.join(tmp)))
174 175
    

176
  def _escape(self, data):
177 178 179 180 181 182 183 184 185 186
    """
      Escape &, <, and > in a string of data.
      This is a copy of the xml.sax.saxutils.escape function.
    """
    if data:
      #data = data.replace("&", "&amp;")
      data = data.replace(">", "&gt;")
      data = data.replace("<", "&lt;")
      return data
    
187
  def toHTML(self):
188
    # Adding header of the table
189 190 191
    if self.binary:
      return '<b>Binary File!</b><br><br><br>'
    
Christophe Dumez's avatar
Christophe Dumez committed
192
    html = '''
193 194 195 196
    <table style="text-align: left; width: 100%%;" border="0" cellpadding="0" cellspacing="0">
  <tbody>
    <tr height="18px">
      <td style="background-color: grey"><b><center>%s</center></b></td>
Christophe Dumez's avatar
Christophe Dumez committed
197
      <td style="background-color: black;" width="2"></td>
198
      <td style="background-color: grey"><b><center>%s</center></b></td>
Christophe Dumez's avatar
Christophe Dumez committed
199
    </tr>'''%(self.old_revision, self.new_revision)
Christophe Dumez's avatar
Christophe Dumez committed
200
    header_color = 'grey'
201
    for child in self.children:
202
      # Adding line number of the modification
Christophe Dumez's avatar
Christophe Dumez committed
203 204 205 206 207 208
      html += '''<tr height="18px"><td style="background-color: %s">&nbsp;</td><td style="background-color: black;" width="2"></td><td style="background-color: %s">&nbsp;</td></tr>    <tr height="18px">
      <td style="background-color: rgb(68, 132, 255);"><b>Line %s</b></td>
      <td style="background-color: black;" width="2"></td>
      <td style="background-color: rgb(68, 132, 255);"><b>Line %s</b></td>
      </tr>'''%(header_color, header_color, child.old_line, child.new_line)
      header_color = 'white'
209 210 211 212 213 214
      # Adding diff of the modification
      old_code_list = child.getOldCodeList()
      new_code_list = child.getNewCodeList()
      i=0
      for old_line_tuple in old_code_list:
        new_line_tuple = new_code_list[i]
Christophe Dumez's avatar
Christophe Dumez committed
215 216
        new_line = new_line_tuple[0] or ' '
        old_line = old_line_tuple[0] or ' '
217 218
        i+=1
        html += '''    <tr height="18px">
Christophe Dumez's avatar
Christophe Dumez committed
219 220 221 222
        <td style="background-color: %s">%s</td>
        <td style="background-color: black;" width="2"></td>
        <td style="background-color: %s">%s</td>
        </tr>'''%(old_line_tuple[1], self._escape(old_line).replace(' ', '&nbsp;').replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'), new_line_tuple[1], self._escape(new_line).replace(' ', '&nbsp;').replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'))
223
    html += '''  </tbody>
Christophe Dumez's avatar
Christophe Dumez committed
224
</table><br><br>'''
225 226 227 228 229 230 231 232 233 234 235 236 237 238
    return html
      

# A code block contains several SubCodeBlocks
class CodeBlock:
  # Members :
  # - old_line : line in old code (before modif)
  # - new line : line in new code (after modif)
  #
  # Methods :
  # - getOldCodeList() : return code before modif
  # - getNewCodeList() : return code after modif
  # Note: the code returned is a list of tuples (code line, background color)

239
  def __init__(self, raw_diff):
240
    # Splitting body and header
241 242
    self.body = os.linesep.join(raw_diff.split(os.linesep)[1:])
    self.header = raw_diff.split(os.linesep)[0]
243
    # Getting modifications lines
244 245
    tmp = re.search('^@@ -\d+', self.header)
    self.old_line = tmp.string[tmp.start():tmp.end()][4:]
Christophe Dumez's avatar
Christophe Dumez committed
246 247
    tmp = re.search('\+\d+', self.header)
    self.new_line = tmp.string[tmp.start():tmp.end()][1:]
248 249
    # Splitting modifications in SubCodeBlocks
    in_modif = False
250
    self.children = []
251
    tmp=[]
252
    for line in self.body.split(os.linesep):
253 254 255 256 257
      if line:
        if (line.startswith('+') or line.startswith('-')):
          if in_modif:
            tmp.append(line)
          else:
258
            self.children.append(SubCodeBlock(os.linesep.join(tmp)))
259 260 261 262
            tmp = [line,]
            in_modif = True
        else:
            if in_modif:
263
              self.children.append(SubCodeBlock(os.linesep.join(tmp)))
264 265 266 267
              tmp = [line,]
              in_modif = False
            else:
              tmp.append(line)
268
    self.children.append(SubCodeBlock(os.linesep.join(tmp)))
269 270
    
  # Return code before modification
271
  def getOldCodeList(self):
272
    tmp = []
273
    for child in self.children:
274 275 276 277
      tmp.extend(child.getOldCodeList())
    return tmp
    
  # Return code after modification
278
  def getNewCodeList(self):
279
    tmp = []
280
    for child in self.children:
281 282 283 284 285
      tmp.extend(child.getNewCodeList())
    return tmp
    
# a SubCodeBlock contain 0 or 1 modification (not more)
class SubCodeBlock:
286
  def __init__(self, code):
287 288
    self.body = code
    self.modification = self._getModif()
Christophe Dumez's avatar
Christophe Dumez committed
289 290
    self.old_code_length = self._getOldCodeLength()
    self.new_code_length = self._getNewCodeLength()
291
    # Choosing background color
292 293 294 295 296 297
    if self.modification == 'none':
      self.color = 'white'
    elif self.modification == 'change':
      self.color = 'rgb(253, 228, 6);'#light orange
    elif self.modification == 'deletion':
      self.color = 'rgb(253, 117, 74);'#light red
Christophe Dumez's avatar
Christophe Dumez committed
298
    else: # addition
299
      self.color = 'rgb(83, 253, 74);'#light green
300
    
301
  def _getModif(self):
302 303
    nb_plus = 0
    nb_minus = 0
304
    for line in self.body.split(os.linesep):
305 306 307 308 309 310
      if line.startswith("-"):
        nb_minus-=1
      elif line.startswith("+"):
        nb_plus+=1
    if (nb_plus==0 and nb_minus==0):
      return 'none'
Christophe Dumez's avatar
Christophe Dumez committed
311 312 313 314
    if (nb_minus==0):
      return 'addition'
    if (nb_plus==0):
      return 'deletion'
315
    return 'change'
Christophe Dumez's avatar
Christophe Dumez committed
316 317 318
      
  def _getOldCodeLength(self):
    nb_lines = 0
319
    for line in self.body.split(os.linesep):
Christophe Dumez's avatar
Christophe Dumez committed
320 321 322 323 324 325
      if not line.startswith("+"):
        nb_lines+=1
    return nb_lines
      
  def _getNewCodeLength(self):
    nb_lines = 0
326
    for line in self.body.split(os.linesep):
Christophe Dumez's avatar
Christophe Dumez committed
327 328 329
      if not line.startswith("-"):
        nb_lines+=1
    return nb_lines
330
  
331
  # Return code before modification
332 333
  def getOldCodeList(self):
    if self.modification=='none':
334
      old_code = [(x, 'white') for x in self.body.split(os.linesep)]
Christophe Dumez's avatar
Christophe Dumez committed
335
    elif self.modification=='change':
336
      old_code = [self._getOldCodeList(x) for x in self.body.split(os.linesep) if self._getOldCodeList(x)[0]]
Christophe Dumez's avatar
Christophe Dumez committed
337 338 339 340
      # we want old_code_list and new_code_list to have the same length
      if(self.old_code_length < self.new_code_length):
        filling = [(None, self.color)]*(self.new_code_length-self.old_code_length)
        old_code.extend(filling)
341
    else: # deletion or addition
342
      old_code = [self._getOldCodeList(x) for x in self.body.split(os.linesep)]
343
    return old_code
344
  
345
  def _getOldCodeList(self, line):
346
    if line.startswith('+'):
347
      return (None, self.color)
348
    if line.startswith('-'):
349 350
      return (' '+line[1:], self.color)
    return (line, self.color)
351 352
  
  # Return code after modification
353 354
  def getNewCodeList(self):
    if self.modification=='none':
355
      new_code = [(x, 'white') for x in self.body.split(os.linesep)]
Christophe Dumez's avatar
Christophe Dumez committed
356
    elif self.modification=='change':
357
      new_code = [self._getNewCodeList(x) for x in self.body.split(os.linesep) if self._getNewCodeList(x)[0]]
Christophe Dumez's avatar
Christophe Dumez committed
358 359 360 361
      # we want old_code_list and new_code_list to have the same length
      if(self.new_code_length < self.old_code_length):
        filling = [(None, self.color)]*(self.old_code_length-self.new_code_length)
        new_code.extend(filling)
362
    else: # deletion or addition
363
      new_code = [self._getNewCodeList(x) for x in self.body.split(os.linesep)]
364
    return new_code
365
  
366
  def _getNewCodeList(self, line):
367
    if line.startswith('-'):
368
      return (None, self.color)
369
    if line.startswith('+'):
370 371
      return (' '+line[1:], self.color)
    return (line, self.color)
372
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
373 374 375 376 377 378 379 380 381 382 383
class SubversionTool(UniqueObject, Folder):
  """The SubversionTool provides a Subversion interface to ERP5.
  """
  id = 'portal_subversion'
  meta_type = 'ERP5 Subversion Tool'
  portal_type = 'Subversion Tool'
  allowed_types = ()

  login_cookie_name = 'erp5_subversion_login'
  ssl_trust_cookie_name = 'erp5_subversion_ssl_trust'
  top_working_path = os.path.join(getConfiguration().instancehome, 'svn')
384

Yoshinori Okuji's avatar
Yoshinori Okuji committed
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
  # Declarative Security
  security = ClassSecurityInfo()

  #
  #   ZMI methods
  #
  manage_options = ( ( { 'label'      : 'Overview'
                        , 'action'     : 'manage_overview'
                        }
                      ,
                      )
                    + Folder.manage_options
                    )

  security.declareProtected( Permissions.ManagePortal, 'manage_overview' )
  manage_overview = DTMLFile( 'explainSubversionTool', _dtmldir )

  # Filter content (ZMI))
  def __init__(self):
      return Folder.__init__(self, SubversionTool.id)

  # Filter content (ZMI))
  def filtered_meta_types(self, user=None):
      # Filters the list of available meta types.
      all = SubversionTool.inheritedAttribute('filtered_meta_types')(self)
      meta_types = []
      for meta_type in self.all_meta_types():
          if meta_type['name'] in self.allowed_types:
              meta_types.append(meta_type)
      return meta_types

  def getTopWorkingPath(self):
    return self.top_working_path

  def _getWorkingPath(self, path):
    path = os.path.abspath(path)
    if not path.startswith(self.top_working_path):
      raise Unauthorized, 'unauthorized access to path %s' % path
    return path
424 425 426 427
    
  def setWorkingDirectory(self, path):
    self.workingDirectory = path
    os.chdir(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
428 429 430 431 432 433 434 435

  def getDefaultUserName(self):
    """Return a default user name.
    """
    name = self.portal_preferences.getPreferredSubversionUserName()
    if not name:
      name = self.portal_membership.getAuthenticatedMember().getUserName()
    return name
Yoshinori Okuji's avatar
Yoshinori Okuji committed
436
    
Christophe Dumez's avatar
Christophe Dumez committed
437 438
  
  # path is the path in svn working copy
439 440
  # return edit_path in zodb to edit it
  # return '#' if no zodb path is found
Christophe Dumez's avatar
Christophe Dumez committed
441 442 443
  def editPath(self, bt, path):
    """Return path to edit file
    """
444 445
    path = path.replace('\\', '/')
    if 'bt' in path.split('/'):
446
      # not in zodb
Christophe Dumez's avatar
Christophe Dumez committed
447
      return '#'
448 449 450
    # if file have been deleted then not in zodb
    if not os.path.exists(path):
      return '#'
451
    svn_path = bt.getPortalObject().portal_preferences.getPreferredSubversionWorkingCopy()
Christophe Dumez's avatar
Christophe Dumez committed
452 453
    if not svn_path:
      raise 'Error: Please set working copy path in Subversion preferences !'
454 455
    svn_path = os.path.join(svn_path, bt.getTitle())
    svn_path = svn_path.replace('\\', '/')
Christophe Dumez's avatar
Christophe Dumez committed
456
    edit_path = path.replace(svn_path, '')
457 458 459
    if edit_path.strip() == '':
      # not in zodb 
      return '#'
460
    if edit_path[0] == '/':
461
      edit_path = edit_path[1:]
462
    edit_path = '/'.join(edit_path.split('/')[1:])
463 464 465
    if edit_path.strip() == '':
      # not in zodb 
      return '#'
Christophe Dumez's avatar
Christophe Dumez committed
466 467 468 469
    tmp = re.search('\\.[\w]+$', edit_path)
    if tmp:
      extension = tmp.string[tmp.start():tmp.end()].strip()
      edit_path = edit_path.replace(extension, '')
470
    edit_path = bt.REQUEST["BASE2"] + '/' + edit_path + '/manage_main'
Christophe Dumez's avatar
Christophe Dumez committed
471 472
    return edit_path
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
473 474 475 476 477 478 479 480
  def _encodeLogin(self, realm, user, password):
    # Encode login information.
    return b64encode(dumps((realm, user, password)))

  def _decodeLogin(self, login):
    # Decode login information.
    return loads(b64decode(login))
    
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
  def setLogin(self, realm, user, password):
    """Set login information.
    """
    # Get existing login information. Filter out old information.
    login_list = []
    request = self.REQUEST
    cookie = request.get(self.login_cookie_name)
    if cookie:
      for login in cookie.split(','):
        if self._decodeLogin(login)[0] != realm:
          login_list.append(login)
    # Set the cookie.
    response = request.RESPONSE
    login_list.append(self._encodeLogin(realm, user, password))
    value = ','.join(login_list)
Christophe Dumez's avatar
Christophe Dumez committed
496
    expires = (DateTime() + 7).toZone('GMT').rfc822()
497
    request.set(self.login_cookie_name, value)
498
    response.setCookie(self.login_cookie_name, value, path = '/', expires = expires)
499

Yoshinori Okuji's avatar
Yoshinori Okuji committed
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
  def _getLogin(self, target_realm):
    request = self.REQUEST
    cookie = request.get(self.login_cookie_name)
    if cookie:
      for login in cookie.split(','):
        realm, user, password = self._decodeLogin(login)
        if target_realm == realm:
          return user, password
    return None, None

  def _encodeSSLTrust(self, trust_dict, permanent=False):
    # Encode login information.
    key_list = trust_dict.keys()
    key_list.sort()
    trust_item_list = tuple([(key, trust_dict[key]) for key in key_list])
    return b64encode(dumps((trust_item_list, permanent)))

  def _decodeSSLTrust(self, trust):
    # Decode login information.
Christophe Dumez's avatar
Christophe Dumez committed
519
    trust_item_list, permanent = loads(b64decode(trust))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
520
    return dict(trust_item_list), permanent
521 522 523 524
  
  def diffHTML(self, file_path):
    raw_diff = self.diff(file_path)
    return DiffFile(raw_diff).toHTML()
Christophe Dumez's avatar
Christophe Dumez committed
525 526
  
  # Display a file content in HTML
Christophe Dumez's avatar
Christophe Dumez committed
527
  def fileHTML(self, bt, file_path):
528 529 530
    if os.path.exists(file_path):
      if os.path.isdir(file_path):
        text = "<b>"+file_path+"</b><hr>"
531
        text += file_path +" is a folder!"
532 533 534
      else:
        head = "<b>"+file_path+"</b>  <a href='"+self.editPath(bt, file_path)+"'><img src='imgs/edit.png' border='0'></a><hr>"
        text = commands.getoutput('enscript -B --color --line-numbers --highlight=html --language=html -o - %s'%file_path)
535
        text = head + os.linesep.join(text.split(os.linesep)[10:-4])
536 537 538
      return text
    else:
      # see if tmp file is here (svn deleted file)
539
      if file_path[-1]==os.sep:
540
        file_path=file_path[:-1]
541 542
      filename = file_path.split(os.sep)[-1]
      tmp_path = os.sep.join(file_path.split(os.sep)[:-1])
543
      tmp_path = os.path.join(tmp_path,'.svn','text-base',filename,'.svn-base')
544 545 546
      if os.path.exists(tmp_path):
        head = "<b>"+tmp_path+"</b> (svn temporary file)<hr>"
        text = commands.getoutput('enscript -B --color --line-numbers --highlight=html --language=html -o - %s'%tmp_path)
547
        text = head + os.linesep.join(text.split(os.linesep)[10:-4])
548 549
      else : # does not exist
        text = "<b>"+file_path+"</b><hr>"
550
        text += file_path +" does not exist!"
551 552
      return text
      
Yoshinori Okuji's avatar
Yoshinori Okuji committed
553 554 555 556 557 558 559 560 561
  security.declareProtected(Permissions.ManagePortal, 'acceptSSLServer')
  def acceptSSLServer(self, trust_dict, permanent=False):
    """Accept a SSL server.
    """
    # Get existing trust information.
    trust_list = []
    request = self.REQUEST
    cookie = request.get(self.ssl_trust_cookie_name)
    if cookie:
Christophe Dumez's avatar
Christophe Dumez committed
562
      trust_list.append(cookie)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
563 564 565 566
    # Set the cookie.
    response = request.RESPONSE
    trust_list.append(self._encodeSSLTrust(trust_dict, permanent))
    value = ','.join(trust_list)
Christophe Dumez's avatar
Christophe Dumez committed
567
    expires = (DateTime() + 7).toZone('GMT').rfc822()
568
    request.set(self.ssl_trust_cookie_name, value)
569
    response.setCookie(self.ssl_trust_cookie_name, value, path = '/', expires = expires)
Christophe Dumez's avatar
Christophe Dumez committed
570 571 572
    
  def acceptSSLPerm(self, trust_dict):
    self.acceptSSLServer(self, trust_dict, True)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595

  def _trustSSLServer(self, target_trust_dict):
    request = self.REQUEST
    cookie = request.get(self.ssl_trust_cookie_name)
    if cookie:
      for trust in cookie.split(','):
        trust_dict, permanent = self._decodeSSLTrust(trust)
        for key in target_trust_dict.keys():
          if target_trust_dict[key] != trust_dict.get(key):
            continue
        else:
          return True, permanent
    return False, False
    
  def _getClient(self, **kw):
    # Get the svn client object.
    return newSubversionClient(self, **kw)

  security.declareProtected('Import/Export objects', 'update')
  def update(self, path):
    """Update a working copy.
    """
    client = self._getClient()
596
    return client.update(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
597 598 599 600 601 602

  security.declareProtected('Import/Export objects', 'add')
  def add(self, path):
    """Add a file or a directory.
    """
    client = self._getClient()
603
    return client.add(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
604

605 606 607 608
  security.declareProtected('Import/Export objects', 'info')
  def info(self):
    """return info of working copy
    """
609
    working_copy = self.getPortalObject().portal_preferences.getPreferredSubversionWorkingCopy()
610 611 612 613 614
    if not working_copy :
      raise 'Please set Working copy path in preferences'
    client = self._getClient()
    return client.info(working_copy)
  
Christophe Dumez's avatar
Christophe Dumez committed
615 616 617 618 619 620 621
  security.declareProtected('Import/Export objects', 'log')
  def log(self, path):
    """return log of a file or dir
    """
    client = self._getClient()
    return client.log(path)
  
622 623 624 625
  security.declareProtected('Import/Export objects', 'cleanup')
  def cleanup(self):
    """remove svn locks in working copy
    """
626
    working_copy = self.getPortalObject().portal_preferences.getPreferredSubversionWorkingCopy()
627 628 629 630 631
    if not working_copy :
      raise 'Please set Working copy path in preferences'
    client = self._getClient()
    return client.cleanup(working_copy)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
632 633 634 635 636
  security.declareProtected('Import/Export objects', 'remove')
  def remove(self, path):
    """Remove a file or a directory.
    """
    client = self._getClient()
637
    return client.remove(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
638 639 640 641 642 643 644 645

  security.declareProtected('Import/Export objects', 'move')
  def move(self, src, dest):
    """Move/Rename a file or a directory.
    """
    client = self._getClient()
    return client.move(src, dest)

Christophe Dumez's avatar
Christophe Dumez committed
646 647 648 649 650 651 652
  security.declareProtected('Import/Export objects', 'ls')
  def ls(self, path):
    """Display infos about a file.
    """
    client = self._getClient()
    return client.ls(path)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
653 654 655 656 657
  security.declareProtected('Import/Export objects', 'diff')
  def diff(self, path):
    """Make a diff for a file or a directory.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
658
    return client.diff(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
659 660 661 662 663 664

  security.declareProtected('Import/Export objects', 'revert')
  def revert(self, path):
    """Revert local changes in a file or a directory.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
665
    return client.revert(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
666 667

  security.declareProtected('Import/Export objects', 'checkin')
668
  def checkin(self, path, log_message=None, recurse=True):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
669 670
    """Commit local changes.
    """
671
    client = self._getClient()
672
    return client.checkin(path, log_message, recurse)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
673 674 675 676 677 678

  security.declareProtected('Import/Export objects', 'status')
  def status(self, path, **kw):
    """Get status.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
679
    return client.status(path, **kw)
680
  
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
  security.declareProtected('Import/Export objects', 'unversionedFiles')
  def unversionedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
    status_list = client.status(path, **kw)
    unversioned_list = []
    for statusObj in status_list:
      if str(statusObj.getTextStatus()) == "unversioned":
        my_dict = {}
        my_dict['uid'] = statusObj.getPath()
        unversioned_list.append(my_dict)
    return unversioned_list
      
  security.declareProtected('Import/Export objects', 'removeAllInList')
  def removeAllInList(self, list):
    """Remove all files and folders in list
    """
    for file in list:
      removeAll(file)
    
702
  def getModifiedTree(self, path) :
Christophe Dumez's avatar
Christophe Dumez committed
703
    # Remove trailing slash if it's present
704
    if path[-1] == os.sep :
705 706
      path = path[:-1]
    
Christophe Dumez's avatar
Christophe Dumez committed
707
    root = Dir(path, "normal")
708
    somethingModified = False
709
    
710
    for statusObj in self.status(path) :
711
      # can be (normal, added, modified, deleted, conflicted, unversioned)
712
      msg_status = statusObj.getTextStatus()
713
      if str(msg_status) != "normal" and str(msg_status) != "unversioned":
714
        somethingModified = True
Christophe Dumez's avatar
Christophe Dumez committed
715
        full_path = statusObj.getPath()
716
        full_path_list = full_path.split(os.sep)[1:]
Christophe Dumez's avatar
Christophe Dumez committed
717
        relative_path = full_path[len(path)+1:]
718
        relative_path_list = relative_path.split(os.sep)
719
        # Processing entry
Christophe Dumez's avatar
Christophe Dumez committed
720 721 722
        filename = relative_path_list[-1]
        # Needed or files will be both File & Dir objects
        relative_path_list = relative_path_list[:-1]
723
        parent = root
724
        i = len(path.split(os.sep))-1
Christophe Dumez's avatar
Christophe Dumez committed
725 726 727 728
        
        for d in relative_path_list :
          i += 1
          if d :
729
            full_pathOfd = os.sep+os.sep.join(full_path_list[:i]).strip()
730
            if d not in parent.getSubDirs() :
731
              parent.sub_dirs.append(Dir(full_pathOfd, "normal"))
732
            parent = parent.getDir(d)
Christophe Dumez's avatar
Christophe Dumez committed
733
        if os.path.isdir(full_path) :
734 735
          if full_path == parent.full_path :
            parent.msg_status = str(msg_status)
736 737
          elif filename not in parent.getSubDirs() :
            parent.sub_dirs.append(Dir(filename, str(msg_status)))
Christophe Dumez's avatar
Christophe Dumez committed
738
          else :
739
            tmp = parent.getDir(filename)
740
            tmp.msg_status = str(msg_status)
Christophe Dumez's avatar
Christophe Dumez committed
741
        else :
Christophe Dumez's avatar
Christophe Dumez committed
742
          parent.sub_dirs.append(File(full_path, str(msg_status)))
743
    return somethingModified and root
744
  
745 746
  def extractBT(self, bt):
    path = mktemp()
747
    bt.export(path=path, local=1)
748
    svn_path = self.getPortalObject().portal_preferences.getPreferredSubversionWorkingCopy()
749
    if not svn_path :
750
      raise "Error: Please set Subversion working path in preferences"
751 752
    svn_path=os.path.join(svn_path,bt.getTitle())+os.sep
    path+=os.sep
753
    # svn del deleted files
754
    self.deleteOldFiles(svn_path, path, bt)
755
    # add new files and copy
756
    self.addNewFiles(svn_path, path, bt)
757
    # Clean up
758
    removeAll(path)
759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775

  # return a set with dirs & files present in the directory
  def getSetForDir(self, directory):
    dir_set = set()
    for root, dirs, files in os.walk(directory):
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
      # get Directories
      for name in dirs:
        f = os.path.join(root, name)
        dir_set.add(f.replace(directory,''))
      # get Files
      for name in files: 
        f = os.path.join(root, name)
        dir_set.add(f.replace(directory,''))
    return dir_set
776
  
777 778 779
  # return files/dirs present in new_dir but not in old_dir
  # return a set of relative paths
  def getNewFiles(self, old_dir, new_dir):
780 781 782 783
    if old_dir[-1] != os.sep:
      old_dir += os.sep
    if new_dir[-1] != os.sep:
      new_dir += os.sep
784 785 786 787
    old_set = self.getSetForDir(old_dir)
    new_set = self.getSetForDir(new_dir)
    return new_set.difference(old_set)

788
  # svn del files that have been removed in new dir
789
  def deleteOldFiles(self, old_dir, new_dir, bt):
790
    # detect removed files
791
    files_set = self.getNewFiles(new_dir, old_dir)
792
    # svn del
793 794
    for file in files_set:
        self.remove(os.path.join(old_dir, file)) 
795
  
796 797
  # copy files and add new files
  def addNewFiles(self, old_dir, new_dir, bt):
798
    # detect created files
799
    files_set = self.getNewFiles(old_dir, new_dir)
800
    # Copy files
801 802
    #os.system('cp -af %s/* %s'%(new_dir, old_dir))
    copytree(new_dir, old_dir)
803
    # svn add
804 805
    for file in files_set:
          self.add(os.path.join(old_dir, file))
806
  
807
  def treeToXML(self, item) :
808 809
    output = "<?xml version='1.0' encoding='iso-8859-1'?>"+ os.linesep
    output += "<tree id='0'>" + os.linesep
Christophe Dumez's avatar
Christophe Dumez committed
810
    output = self._treeToXML(item, output, 1, True)
811 812
    output += "</tree>" + os.linesep
    return output
813
  
Christophe Dumez's avatar
Christophe Dumez committed
814
  def _treeToXML(self, item, output, ident, first) :
815
    # svn path
816
    svn_path = self.getPortalObject().portal_preferences.getPreferredSubversionWorkingCopy()
817 818
    if not svn_path :
      raise "Error: Please set Subversion working path in preferences"
819 820
    if svn_path[-1] != os.sep:
      svn_path += os.sep
821
    # Choosing a color coresponding to the status
822
    itemStatus = item.msg_status
Christophe Dumez's avatar
Christophe Dumez committed
823 824
    if itemStatus == 'added' :
      itemColor='green'
825
    elif itemStatus == 'modified' or  itemStatus == 'replaced' :
Christophe Dumez's avatar
Christophe Dumez committed
826 827 828
      itemColor='orange'
    elif itemStatus == 'deleted' :
      itemColor='red'
829 830
    elif itemStatus == 'conflicted' :
      itemColor='grey'
Christophe Dumez's avatar
Christophe Dumez committed
831 832
    else :
      itemColor='black'
833
      
834 835
    if isinstance(item, Dir) :
      for i in range(ident) :
836
        output += '\t'
Christophe Dumez's avatar
Christophe Dumez committed
837
      if first :
838
        output += '<item open="1" text="%s" id="%s" aCol="%s" '\
Christophe Dumez's avatar
Christophe Dumez committed
839
        'im0="folder.png" im1="folder_open.png" '\
840
        'im2="folder.png">'%(item.name,
841
item.full_path.replace(svn_path, ''), itemColor,) + os.linesep
Christophe Dumez's avatar
Christophe Dumez committed
842 843
        first=False
      else :
844
        output += '<item text="%s" id="%s" aCol="%s" im0="folder.png" ' \
845
      'im1="folder_open.png" im2="folder.png">'%(item.name,
846
item.full_path.replace(svn_path, ''), itemColor,) + os.linesep
847
      for it in item.sub_dirs:
848
        ident += 1
849
        output = self._treeToXML(item.getDir(it.name), output, ident,
Christophe Dumez's avatar
Christophe Dumez committed
850
first)
851 852
        ident -= 1
      for i in range(ident) :
853 854
        output += '\t'
      output += '</item>' + os.linesep
855 856
    else :
      for i in range(ident) :
857 858
        output += '\t'
      output += '<item text="%s" id="%s" aCol="%s" im0="document.png"/>'\
859
                %(item.name, item.full_path.replace(svn_path, ''), itemColor,) + os.linesep
860
    return output
Yoshinori Okuji's avatar
Yoshinori Okuji committed
861 862
    
InitializeClass(SubversionTool)