SubversionTool.py 44.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
#
# 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
31
from Products.ERP5Type.Tool.BaseTool import BaseTool
Yoshinori Okuji's avatar
Yoshinori Okuji committed
32 33 34 35 36 37
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
Christophe Dumez's avatar
Christophe Dumez committed
38
import os, re, commands, time, exceptions, pysvn
Yoshinori Okuji's avatar
Yoshinori Okuji committed
39 40 41 42
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
43 44
from OFS.Image import manage_addFile
from cStringIO import StringIO
45
from tempfile import mktemp
46
from shutil import copy
47
from Products.CMFCore.utils import getToolByName
Christophe Dumez's avatar
Christophe Dumez committed
48 49
from Products.ERP5.Document.BusinessTemplate import removeAll
from Products.ERP5.Document.BusinessTemplate import TemplateConditionError
50
from xml.sax.saxutils import escape
51
from dircache import listdir
52
from OFS.Traversable import NotFound
Aurel's avatar
Aurel committed
53 54 55 56

try:
  from base64 import b64encode, b64decode
except ImportError:
57
  from base64 import encodestring as b64encode, decodestring as b64decode
58 59 60 61 62 63
  
# To keep compatibility with python 2.3
try:
  set
except NameError:
  from sets import Set as set
Christophe Dumez's avatar
Christophe Dumez committed
64 65 66 67

NBSP = '&nbsp;'
NBSP_TAB = NBSP*8

68
class File(object):
Christophe Dumez's avatar
Christophe Dumez committed
69 70
  """ Class that represents a file in memory
  """
71 72 73 74 75 76 77 78
  __slots__ = ('status','name')
  # Constructor
  def __init__(self, name, status) :
    self.status = status
    self.name = name
## End of File Class

class Dir(object):
Christophe Dumez's avatar
Christophe Dumez committed
79 80 81
  """ Class that reprensents a folder in memory
  """
  __slots__ = ('status', 'name', 'sub_dirs', 'sub_files')
82 83 84 85 86 87 88 89
  # Constructor
  def __init__(self, name, status) :
    self.status = status
    self.name = name
    self.sub_dirs = [] # list of sub directories
    self.sub_files = [] # list of sub files

  def getSubDirsNameList(self) :
Christophe Dumez's avatar
Christophe Dumez committed
90 91
    """ return a list of sub directories' names
    """
92 93 94
    return [d.name for d in self.sub_dirs]

  def getDirFromName(self, name):
Christophe Dumez's avatar
Christophe Dumez committed
95 96 97 98 99
    """ return directory in subdirs given its name
    """
    for directory in self.sub_dirs:
      if directory.name == name:
        return directory
100 101
      
  def getObjectFromName(self, name):
Christophe Dumez's avatar
Christophe Dumez committed
102 103 104 105 106 107 108 109
    """ return dir object given its name
    """
    for directory in self.sub_dirs:
      if directory.name == name:
        return directory
    for sub_file in self.sub_files:
      if sub_file.name == name:
        return sub_file
110 111
      
  def getContent(self):
Christophe Dumez's avatar
Christophe Dumez committed
112 113
    """ return content for directory
    """
114 115 116 117
    content = self.sub_dirs
    content.extend(self.sub_files)
    return content
## End of Dir Class
118 119

class Error(exceptions.EnvironmentError):
Christophe Dumez's avatar
Christophe Dumez committed
120 121 122
  """ Simple Exception
  """
  pass
123

124 125 126 127 128 129 130 131 132
class SubversionPreferencesError(Exception):
  """The base exception class for the Subversion preferences.
  """
  pass
  
class SubversionUnknownBusinessTemplateError(Exception):
  """The base exception class when business template is unknown.
  """
  pass
133 134 135 136 137

class SubversionNotAWorkingCopyError(Exception):
  """The base exception class when business template is unknown.
  """
  pass
138 139
      
def copytree(src, dst, symlinks=False):
Christophe Dumez's avatar
Christophe Dumez committed
140
  """Recursively copy a directory tree using copy().
141

Christophe Dumez's avatar
Christophe Dumez committed
142 143
  If exception(s) occur, an Error is raised with a list of reasons.
  dst dir must exist
144

Christophe Dumez's avatar
Christophe Dumez committed
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
  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.
  
  Copyright (c) 2001, 2002, 2003, 2004 Python Software Foundation; All Rights Reserved
  """
  names = 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):
        if not os.path.exists(dstname):
          os.makedirs(dstname)
        copytree(srcname, dstname, symlinks)
      else:
        copy(srcname, dstname)
    except (IOError, os.error), why:
      errors.append((srcname, dstname, 'Error: ' + str(why.strerror)))
  if errors:
    raise Error, errors
171
    
172 173
def cacheWalk(top, topdown=True, onerror=None):
  """Directory tree generator.
174 175 176 177 178

  modification of os.path.walk to use dircache.listdir
  instead of os.path.listdir
  
  Copyright (c) 2001, 2002, 2003, 2004 Python Software Foundation; All Rights Reserved
179 180
  """
  try:
Christophe Dumez's avatar
Christophe Dumez committed
181 182 183 184 185 186 187
    # Note that listdir and error are globals in this module due
    # to earlier import-*.
    names = listdir(top)
  except os.error, err:
    if onerror is not None:
      onerror(err)
    return
188 189 190

  dirs, nondirs = [], []
  for name in names:
Christophe Dumez's avatar
Christophe Dumez committed
191 192 193 194
    if os.path.isdir(os.path.join(top, name)):
      dirs.append(name)
    else:
      nondirs.append(name)
195 196

  if topdown:
Christophe Dumez's avatar
Christophe Dumez committed
197
    yield top, dirs, nondirs
198
  for name in dirs:
Christophe Dumez's avatar
Christophe Dumez committed
199 200 201 202
    path = os.path.join(top, name)
    if not os.path.islink(path):
      for elem in cacheWalk(path, topdown, onerror):
        yield elem
203
  if not topdown:
Christophe Dumez's avatar
Christophe Dumez committed
204
    yield top, dirs, nondirs
205 206

    
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
def colorizeTag(tag):
  "Return html colored item"
  text = tag.group()
  if text.startswith('#') :
    color = 'grey'
  elif text.startswith('\"') :
    color = 'red'
  elif 'string' in text:
    color = 'green'
  elif 'tuple' in text:
    color = 'orange'
  elif 'dictionary' in text:
    color = 'brown'
  elif 'item' in text:
    color = '#A1559A' #light purple
  elif 'value' in text:
    color = 'purple'
  elif 'key' in text:
    color = '#0C4F0C'#dark green
  else:
Christophe Dumez's avatar
Christophe Dumez committed
227 228
    color = 'blue'
  return "<font color='%s'>%s</font>" % (color, text, )
229 230 231 232 233 234
    
def colorize(text):
  """Return HTML Code with syntax hightlighting
  """
  # Escape xml before adding html tags
  html = escape(text)
Christophe Dumez's avatar
Christophe Dumez committed
235 236
  html = html.replace(' ', NBSP)
  html = html.replace('\t', NBSP_TAB)
237
  # Colorize comments
Christophe Dumez's avatar
Christophe Dumez committed
238 239
  pattern = re.compile(r'#.*')
  html = pattern.sub(colorizeTag, html)
240
  # Colorize tags
Christophe Dumez's avatar
Christophe Dumez committed
241 242
  pattern = re.compile(r'&lt;.*?&gt;')
  html = pattern.sub(colorizeTag, html)
243
  # Colorize strings
Christophe Dumez's avatar
Christophe Dumez committed
244 245
  pattern = re.compile(r'\".*?\"')
  html = pattern.sub(colorizeTag, html)
246 247
  html = html.replace(os.linesep, os.linesep+"<br>")
  return html
248 249

class DiffFile:
Christophe Dumez's avatar
Christophe Dumez committed
250
  """
251
  # Members :
Christophe Dumez's avatar
Christophe Dumez committed
252 253 254 255 256
   - path : path of the modified file
   - children : sub codes modified
   - old_revision
   - new_revision
  """
257

258
  def __init__(self, raw_diff):
259
    if '@@' not in raw_diff:
Christophe Dumez's avatar
Christophe Dumez committed
260
      self.binary = True
261 262
      return
    else:
Christophe Dumez's avatar
Christophe Dumez committed
263
      self.binary = False
264
    self.header = raw_diff.split('@@')[0][:-1]
265
    # Getting file path in header
266
    self.path = self.header.split('====')[0][:-1].strip()
267
    # Getting revisions in header
268
    for line in self.header.split(os.linesep):
269
      if line.startswith('--- '):
270
        tmp = re.search('\\([^)]+\\)$', line)
271
        self.old_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip()
272
      if line.startswith('+++ '):
273
        tmp = re.search('\\([^)]+\\)$', line)
274
        self.new_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip()
275
    # Splitting the body from the header
276
    self.body = os.linesep.join(raw_diff.strip().split(os.linesep)[4:])
277
    # Now splitting modifications
278
    self.children = []
279 280
    first = True
    tmp = []
281
    for line in self.body.split(os.linesep):
282 283
      if line:
        if line.startswith('@@') and not first:
284
          self.children.append(CodeBlock(os.linesep.join(tmp)))
Christophe Dumez's avatar
Christophe Dumez committed
285
          tmp = [line, ]
286 287 288
        else:
          first = False
          tmp.append(line)
289
    self.children.append(CodeBlock(os.linesep.join(tmp)))
290
    
291
  def toHTML(self):
Christophe Dumez's avatar
Christophe Dumez committed
292 293
    """ return HTML diff
    """
294
    # Adding header of the table
295
    if self.binary:
Christophe Dumez's avatar
Christophe Dumez committed
296
      return '<b>Folder or binary file or just no changes!</b><br><br><br>'
297
    
Christophe Dumez's avatar
Christophe Dumez committed
298 299
    html_list = []
    html_list.append('''
300 301 302 303
    <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
304
      <td style="background-color: black;" width="2"></td>
305
      <td style="background-color: grey"><b><center>%s</center></b></td>
Christophe Dumez's avatar
Christophe Dumez committed
306
    </tr>''' % (self.old_revision, self.new_revision))
Christophe Dumez's avatar
Christophe Dumez committed
307
    header_color = 'grey'
Christophe Dumez's avatar
Christophe Dumez committed
308 309 310 311 312 313 314
    child_html_text = '''<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)
315
    for child in self.children:
316
      # Adding line number of the modification
Christophe Dumez's avatar
Christophe Dumez committed
317
      html_list.append( child_html_text % (child.old_line, child.new_line) )
Christophe Dumez's avatar
Christophe Dumez committed
318
      header_color = 'white'
319 320 321
      # Adding diff of the modification
      old_code_list = child.getOldCodeList()
      new_code_list = child.getNewCodeList()
Christophe Dumez's avatar
Christophe Dumez committed
322
      i = 0
323 324
      for old_line_tuple in old_code_list:
        new_line_tuple = new_code_list[i]
Christophe Dumez's avatar
Christophe Dumez committed
325 326
        new_line = new_line_tuple[0] or ' '
        old_line = old_line_tuple[0] or ' '
Christophe Dumez's avatar
Christophe Dumez committed
327 328
        i += 1
        html_list.append( '''<tr height="18px">
Christophe Dumez's avatar
Christophe Dumez committed
329 330 331
        <td style="background-color: %s">%s</td>
        <td style="background-color: black;" width="2"></td>
        <td style="background-color: %s">%s</td>
Christophe Dumez's avatar
Christophe Dumez committed
332 333 334 335 336 337 338
        </tr>'''%(old_line_tuple[1],
        escape(old_line).replace(' ', NBSP).replace('\t', NBSP_TAB),
        new_line_tuple[1],
        escape(new_line).replace(' ', NBSP).replace('\t', NBSP_TAB))
        )
    html_list.append('''  </tbody></table><br/><br/>''')
    return '\n'.join(html_list)
339 340 341
      

class CodeBlock:
Christophe Dumez's avatar
Christophe Dumez committed
342 343 344 345 346 347 348 349 350 351 352
  """
   A code block contains several SubCodeBlocks
   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)
  """
353

354
  def __init__(self, raw_diff):
355
    # Splitting body and header
356 357
    self.body = os.linesep.join(raw_diff.split(os.linesep)[1:])
    self.header = raw_diff.split(os.linesep)[0]
358
    # Getting modifications lines
359 360
    tmp = re.search('^@@ -\d+', self.header)
    self.old_line = tmp.string[tmp.start():tmp.end()][4:]
Christophe Dumez's avatar
Christophe Dumez committed
361 362
    tmp = re.search('\+\d+', self.header)
    self.new_line = tmp.string[tmp.start():tmp.end()][1:]
363 364
    # Splitting modifications in SubCodeBlocks
    in_modif = False
365
    self.children = []
Christophe Dumez's avatar
Christophe Dumez committed
366
    tmp = []
367
    for line in self.body.split(os.linesep):
368 369 370 371 372
      if line:
        if (line.startswith('+') or line.startswith('-')):
          if in_modif:
            tmp.append(line)
          else:
373
            self.children.append(SubCodeBlock(os.linesep.join(tmp)))
Christophe Dumez's avatar
Christophe Dumez committed
374
            tmp = [line, ]
375 376
            in_modif = True
        else:
Christophe Dumez's avatar
Christophe Dumez committed
377 378 379 380 381 382
          if in_modif:
            self.children.append(SubCodeBlock(os.linesep.join(tmp)))
            tmp = [line, ]
            in_modif = False
          else:
            tmp.append(line)
383
    self.children.append(SubCodeBlock(os.linesep.join(tmp)))
384
    
385
  def getOldCodeList(self):
Christophe Dumez's avatar
Christophe Dumez committed
386 387
    """ Return code before modification
    """
388
    tmp = []
389
    for child in self.children:
390 391 392
      tmp.extend(child.getOldCodeList())
    return tmp
    
393
  def getNewCodeList(self):
Christophe Dumez's avatar
Christophe Dumez committed
394 395
    """ Return code after modification
    """
396
    tmp = []
397
    for child in self.children:
398 399 400 401
      tmp.extend(child.getNewCodeList())
    return tmp
    
class SubCodeBlock:
Christophe Dumez's avatar
Christophe Dumez committed
402 403
  """ a SubCodeBlock contain 0 or 1 modification (not more)
  """
404
  def __init__(self, code):
405 406
    self.body = code
    self.modification = self._getModif()
Christophe Dumez's avatar
Christophe Dumez committed
407 408
    self.old_code_length = self._getOldCodeLength()
    self.new_code_length = self._getNewCodeLength()
409
    # Choosing background color
410 411 412 413 414 415
    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
416
    else: # addition
417
      self.color = 'rgb(83, 253, 74);'#light green
418
    
419
  def _getModif(self):
Christophe Dumez's avatar
Christophe Dumez committed
420 421 422
    """ Return type of modification :
        addition, deletion, none
    """
423 424
    nb_plus = 0
    nb_minus = 0
425
    for line in self.body.split(os.linesep):
426
      if line.startswith("-"):
Christophe Dumez's avatar
Christophe Dumez committed
427
        nb_minus -= 1
428
      elif line.startswith("+"):
Christophe Dumez's avatar
Christophe Dumez committed
429 430
        nb_plus += 1
    if (nb_plus == 0 and nb_minus == 0):
431
      return 'none'
Christophe Dumez's avatar
Christophe Dumez committed
432
    if (nb_minus == 0):
Christophe Dumez's avatar
Christophe Dumez committed
433
      return 'addition'
Christophe Dumez's avatar
Christophe Dumez committed
434
    if (nb_plus == 0):
Christophe Dumez's avatar
Christophe Dumez committed
435
      return 'deletion'
436
    return 'change'
Christophe Dumez's avatar
Christophe Dumez committed
437 438
      
  def _getOldCodeLength(self):
Christophe Dumez's avatar
Christophe Dumez committed
439 440
    """ Private function to return old code length
    """
Christophe Dumez's avatar
Christophe Dumez committed
441
    nb_lines = 0
442
    for line in self.body.split(os.linesep):
Christophe Dumez's avatar
Christophe Dumez committed
443
      if not line.startswith("+"):
Christophe Dumez's avatar
Christophe Dumez committed
444
        nb_lines += 1
Christophe Dumez's avatar
Christophe Dumez committed
445 446 447
    return nb_lines
      
  def _getNewCodeLength(self):
Christophe Dumez's avatar
Christophe Dumez committed
448 449
    """ Private function to return new code length
    """
Christophe Dumez's avatar
Christophe Dumez committed
450
    nb_lines = 0
451
    for line in self.body.split(os.linesep):
Christophe Dumez's avatar
Christophe Dumez committed
452
      if not line.startswith("-"):
Christophe Dumez's avatar
Christophe Dumez committed
453
        nb_lines += 1
Christophe Dumez's avatar
Christophe Dumez committed
454
    return nb_lines
455
  
456
  def getOldCodeList(self):
Christophe Dumez's avatar
Christophe Dumez committed
457 458 459
    """ Return code before modification
    """
    if self.modification == 'none':
460
      old_code = [(x, 'white') for x in self.body.split(os.linesep)]
Christophe Dumez's avatar
Christophe Dumez committed
461 462 463
    elif self.modification == 'change':
      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
464 465
      # we want old_code_list and new_code_list to have the same length
      if(self.old_code_length < self.new_code_length):
Christophe Dumez's avatar
Christophe Dumez committed
466 467
        filling = [(None, self.color)] * (self.new_code_length - \
        self.old_code_length)
Christophe Dumez's avatar
Christophe Dumez committed
468
        old_code.extend(filling)
469
    else: # deletion or addition
470
      old_code = [self._getOldCodeList(x) for x in self.body.split(os.linesep)]
471
    return old_code
472
  
473
  def _getOldCodeList(self, line):
Christophe Dumez's avatar
Christophe Dumez committed
474 475
    """ Private function to return code before modification
    """
476
    if line.startswith('+'):
477
      return (None, self.color)
478
    if line.startswith('-'):
Christophe Dumez's avatar
Christophe Dumez committed
479
      return (' ' + line[1:], self.color)
480
    return (line, self.color)
481
  
482
  def getNewCodeList(self):
Christophe Dumez's avatar
Christophe Dumez committed
483 484 485
    """ Return code after modification
    """
    if self.modification == 'none':
486
      new_code = [(x, 'white') for x in self.body.split(os.linesep)]
Christophe Dumez's avatar
Christophe Dumez committed
487 488 489
    elif self.modification == 'change':
      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
490 491
      # we want old_code_list and new_code_list to have the same length
      if(self.new_code_length < self.old_code_length):
Christophe Dumez's avatar
Christophe Dumez committed
492 493
        filling = [(None, self.color)] * (self.old_code_length - \
        self.new_code_length)
Christophe Dumez's avatar
Christophe Dumez committed
494
        new_code.extend(filling)
495
    else: # deletion or addition
496
      new_code = [self._getNewCodeList(x) for x in self.body.split(os.linesep)]
497
    return new_code
498
  
499
  def _getNewCodeList(self, line):
Christophe Dumez's avatar
Christophe Dumez committed
500 501
    """ Private function to return code after modification
    """
502
    if line.startswith('-'):
503
      return (None, self.color)
504
    if line.startswith('+'):
Christophe Dumez's avatar
Christophe Dumez committed
505
      return (' ' + line[1:], self.color)
506
    return (line, self.color)
507
  
508
class SubversionTool(BaseTool, UniqueObject, Folder):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
509 510 511 512 513 514 515 516 517
  """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'
518 519 520
  
  top_working_path = getConfiguration().instancehome
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
  # 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):
Christophe Dumez's avatar
Christophe Dumez committed
540 541
    return Folder.__init__(self, SubversionTool.id)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
542 543

  def filtered_meta_types(self, user=None):
Christophe Dumez's avatar
Christophe Dumez committed
544 545 546 547 548 549 550 551 552 553
    """
     Filter content (ZMI))
     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
Yoshinori Okuji's avatar
Yoshinori Okuji committed
554
    
Christophe Dumez's avatar
Christophe Dumez committed
555
  # path is the path in svn working copy
556 557
  # return edit_path in zodb to edit it
  # return '#' if no zodb path is found
Christophe Dumez's avatar
Christophe Dumez committed
558
  def editPath(self, business_template, path):
Christophe Dumez's avatar
Christophe Dumez committed
559
    """Return path to edit file
560
       path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
561
    """
Christophe Dumez's avatar
Christophe Dumez committed
562
    path = self.relativeToAbsolute(path, business_template).replace('\\', '/')
563
    if 'bt' in path.split('/'):
564
      # not in zodb
Christophe Dumez's avatar
Christophe Dumez committed
565
      return '#'
566 567 568
    # if file have been deleted then not in zodb
    if not os.path.exists(path):
      return '#'
Christophe Dumez's avatar
Christophe Dumez committed
569
    svn_path = self.getSubversionPath(business_template).replace('\\', '/')
Christophe Dumez's avatar
Christophe Dumez committed
570 571
    edit_path = path.replace(svn_path, '').strip()
    if edit_path == '':
572 573
      # not in zodb 
      return '#'
574
    if edit_path[0] == '/':
575
      edit_path = edit_path[1:]
Christophe Dumez's avatar
Christophe Dumez committed
576 577
    edit_path = '/'.join(edit_path.split('/')[1:]).strip()
    if edit_path == '':
578 579
      # not in zodb 
      return '#'
580
    # remove file extension
Christophe Dumez's avatar
Christophe Dumez committed
581 582
    edit_path = os.path.splitext(edit_path)[0]
    # Add beginning and end of url
Christophe Dumez's avatar
Christophe Dumez committed
583 584
    edit_path = os.path.join(business_template.REQUEST["BASE2"], \
    edit_path, 'manage_main')
Christophe Dumez's avatar
Christophe Dumez committed
585 586
    return edit_path
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
587
  def _encodeLogin(self, realm, user, password):
Christophe Dumez's avatar
Christophe Dumez committed
588 589
    """ Encode login information.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
590 591 592
    return b64encode(dumps((realm, user, password)))

  def _decodeLogin(self, login):
Christophe Dumez's avatar
Christophe Dumez committed
593 594
    """ Decode login information.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
595
    return loads(b64decode(login))
596
  
Christophe Dumez's avatar
Christophe Dumez committed
597 598 599 600 601
  def goToWorkingCopy(self, business_template):
    """ Change to business template directory
    """
    working_path = self.getSubversionPath(business_template)
    os.chdir(working_path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
602
    
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
  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)
618
    expires = (DateTime() + 1).toZone('GMT').rfc822()
619
    request.set(self.login_cookie_name, value)
Christophe Dumez's avatar
Christophe Dumez committed
620 621
    response.setCookie(self.login_cookie_name, value, path = '/', \
    expires = expires)
622

Yoshinori Okuji's avatar
Yoshinori Okuji committed
623 624 625 626 627 628 629 630 631
  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
632
      
Christophe Dumez's avatar
Christophe Dumez committed
633 634 635 636 637
  def getHeader(self, business_template, file_path):
    file_path = self.relativeToAbsolute(file_path, business_template)
    header = "<b><a href='BusinessTemplate_viewSvnShowFile?file=" + \
    file_path + "'>" + file_path + "</a></b>"
    edit_path = self.editPath(business_template, file_path)
638
    if edit_path != '#':
Christophe Dumez's avatar
Christophe Dumez committed
639 640
      header += "&nbsp;&nbsp;<a href='"+self.editPath(business_template, \
      file_path) + "'><img src='imgs/edit.png' border='0'></a>"
641
    return header
Yoshinori Okuji's avatar
Yoshinori Okuji committed
642 643 644 645 646 647 648 649 650 651

  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
652
    trust_item_list, permanent = loads(b64decode(trust))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
653
    return dict(trust_item_list), permanent
654
  
655 656 657
  def getPreferredUsername(self):
    """return username in preferences if set of the current username
    """
Christophe Dumez's avatar
Christophe Dumez committed
658 659
    username = self.getPortalObject().portal_preferences\
    .getPreferredSubversionUserName()
660 661 662 663 664
    if username is None or username.strip() == "":
      # not set in preferences, then we get the current username in zope
      username = self.portal_membership.getAuthenticatedMember().getUserName()
    return username
  
Christophe Dumez's avatar
Christophe Dumez committed
665 666 667 668 669
  def diffHTML(self, file_path, business_template, revision1=None, \
  revision2=None):
    """ Return HTML diff
    """
    raw_diff = self.diff(file_path, business_template, revision1, revision2)
670
    return DiffFile(raw_diff).toHTML()
Christophe Dumez's avatar
Christophe Dumez committed
671
  
Christophe Dumez's avatar
Christophe Dumez committed
672 673 674 675 676
  def fileHTML(self, business_template, file_path):
    """ Display a file content in HTML with syntax highlighting
    """
    file_path = self.relativeToAbsolute(file_path, business_template)
    input_file = open(file_path, 'r')
677 678 679
    if os.path.exists(file_path):
      if os.path.isdir(file_path):
        text = "<b>"+file_path+"</b><hr>"
680
        text += file_path +" is a folder!"
681
      else:
Christophe Dumez's avatar
Christophe Dumez committed
682 683 684 685
        head = "<b>"+file_path+"</b>  <a href='" + \
        self.editPath(business_template, file_path) + \
        "'><img src='imgs/edit.png' border='0'></a><hr>"
        text = head + colorize(input_file.read())
686 687
    else:
      # see if tmp file is here (svn deleted file)
Christophe Dumez's avatar
Christophe Dumez committed
688 689
      if file_path[-1] == os.sep:
        file_path = file_path[:-1]
690 691
      filename = file_path.split(os.sep)[-1]
      tmp_path = os.sep.join(file_path.split(os.sep)[:-1])
Christophe Dumez's avatar
Christophe Dumez committed
692 693
      tmp_path = os.path.join(tmp_path, '.svn', 'text-base', \
      filename+'.svn-base')
694 695
      if os.path.exists(tmp_path):
        head = "<b>"+tmp_path+"</b> (svn temporary file)<hr>"
Christophe Dumez's avatar
Christophe Dumez committed
696
        text = head + colorize(input_file.read())
697 698
      else : # does not exist
        text = "<b>"+file_path+"</b><hr>"
699
        text += file_path +" does not exist!"
Christophe Dumez's avatar
Christophe Dumez committed
700
    input_file.close()
701
    return text
702
      
Yoshinori Okuji's avatar
Yoshinori Okuji committed
703 704 705 706 707 708 709 710 711
  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
712
      trust_list.append(cookie)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
713 714 715 716
    # Set the cookie.
    response = request.RESPONSE
    trust_list.append(self._encodeSSLTrust(trust_dict, permanent))
    value = ','.join(trust_list)
717
    expires = (DateTime() + 1).toZone('GMT').rfc822()
718
    request.set(self.ssl_trust_cookie_name, value)
Christophe Dumez's avatar
Christophe Dumez committed
719 720
    response.setCookie(self.ssl_trust_cookie_name, value, path = '/', \
    expires = expires)
Christophe Dumez's avatar
Christophe Dumez committed
721 722
    
  def acceptSSLPerm(self, trust_dict):
Christophe Dumez's avatar
Christophe Dumez committed
723 724
    """ Accept SSL server permanently
    """
Christophe Dumez's avatar
Christophe Dumez committed
725
    self.acceptSSLServer(self, trust_dict, True)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742

  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)
743 744
  
  security.declareProtected('Import/Export objects', 'getSubversionPath')
Christophe Dumez's avatar
Christophe Dumez committed
745 746 747 748 749 750 751 752 753 754 755
  def getSubversionPath(self, business_template, with_name=True):
    """
     return the working copy path corresponding to
     the given business template browsing
     working copy list in preferences (looking
     only at first level of directories)
     
     with_name : with business template name at the end of the path
    """
    wc_list = self.getPortalObject().portal_preferences\
    .getPreferredSubversionWorkingCopyList()
756
    if not wc_list:
Christophe Dumez's avatar
Christophe Dumez committed
757 758
      wc_list = self.getPortalObject().portal_preferences.\
      default_site_preference.getPreferredSubversionWorkingCopyList()
759
      if not wc_list:
Christophe Dumez's avatar
Christophe Dumez committed
760 761
        raise SubversionPreferencesError, \
        'Please set at least one Subversion Working Copy in preferences first.'
762
    if len(wc_list) == 0 :
Christophe Dumez's avatar
Christophe Dumez committed
763 764 765 766 767 768 769 770 771 772 773
      raise SubversionPreferencesError, \
      'Please set at least one Subversion Working Copy in preferences first.'
    bt_name = business_template.getTitle()
    for working_copy in wc_list:
      working_copy = self._getWorkingPath(working_copy)
      if not os.path.exists(os.path.join(working_copy, '.svn')):
        raise SubversionNotAWorkingCopyError, \
        "You must check out working copies in this directory: " + \
        working_copy + " or choose another path in portal preferences."
      if bt_name in listdir(working_copy) :
        wc_path = os.path.join(working_copy, bt_name)
774 775 776 777 778
        if os.path.isdir(wc_path):
          if with_name:
            return wc_path
          else:
            return os.sep.join(wc_path.split(os.sep)[:-1])
Christophe Dumez's avatar
Christophe Dumez committed
779 780
    raise SubversionUnknownBusinessTemplateError, "Could not find '"+\
    bt_name+"' at first level of working copies."
781 782

  def _getWorkingPath(self, path):
Christophe Dumez's avatar
Christophe Dumez committed
783 784
    """ Check if the given path is reachable (allowed)
    """
785 786 787 788
    if not path.startswith(self.top_working_path):
      raise Unauthorized, 'unauthorized access to path %s' % path
    return path
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
789
  security.declareProtected('Import/Export objects', 'update')
Christophe Dumez's avatar
Christophe Dumez committed
790
  def update(self, business_template):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
791 792
    """Update a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
793
    path = self._getWorkingPath(self.getSubversionPath(business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
794
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
795 796
    # Revert local changes in working copy first 
    # to import a "pure" BT after update
797 798 799 800
    self.revert(path=path, recurse=True)
    # Update from SVN
    client.update(path)
    # Import in zodb
Christophe Dumez's avatar
Christophe Dumez committed
801
    return self.importBT(business_template)
802
  
Christophe Dumez's avatar
Christophe Dumez committed
803 804
  security.declareProtected('Import/Export objects', 'updatewc')
  def updatewc(self, business_template):
805 806
    """Update a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
807
    path = self._getWorkingPath(self.getSubversionPath(business_template))
808 809 810
    client = self._getClient()
    # Update from SVN
    client.update(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
811

812
  security.declareProtected('Import/Export objects', 'switch')
Christophe Dumez's avatar
Christophe Dumez committed
813
  def switch(self, business_template, url):
814 815
    """switch SVN repository for a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
816
    path = self._getWorkingPath(self.getSubversionPath(business_template))
817
    client = self._getClient()
818 819
    if url[-1] == '/' :
      url = url[:-1]
820
    # Update from SVN
821
    client.switch(path=path, url=url)
822
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
823
  security.declareProtected('Import/Export objects', 'add')
824
  # path can be a list or not (relative or absolute)
Christophe Dumez's avatar
Christophe Dumez committed
825
  def add(self, path, business_template=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
826 827
    """Add a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
828
    if business_template is not None:
829
      if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
830 831
        path = [self._getWorkingPath(self.relativeToAbsolute(x, \
        business_template)) for x in path]
832
      else:
Christophe Dumez's avatar
Christophe Dumez committed
833 834
        path = self._getWorkingPath(self.relativeToAbsolute(path, \
        business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
835
    client = self._getClient()
836
    return client.add(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
837

838
  security.declareProtected('Import/Export objects', 'info')
Christophe Dumez's avatar
Christophe Dumez committed
839
  def info(self, business_template):
840 841
    """return info of working copy
    """
Christophe Dumez's avatar
Christophe Dumez committed
842 843
    working_copy = self._getWorkingPath(self\
    .getSubversionPath(business_template))
844 845 846
    client = self._getClient()
    return client.info(working_copy)
  
Christophe Dumez's avatar
Christophe Dumez committed
847
  security.declareProtected('Import/Export objects', 'log')
848
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
849
  def log(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
850 851 852
    """return log of a file or dir
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
853 854
    return client.log(self._getWorkingPath(self\
    .relativeToAbsolute(path, business_template)))
Christophe Dumez's avatar
Christophe Dumez committed
855
  
856
  security.declareProtected('Import/Export objects', 'cleanup')
Christophe Dumez's avatar
Christophe Dumez committed
857
  def cleanup(self, business_template):
858 859
    """remove svn locks in working copy
    """
Christophe Dumez's avatar
Christophe Dumez committed
860 861
    working_copy = self._getWorkingPath(self\
    .getSubversionPath(business_template))
862 863 864
    client = self._getClient()
    return client.cleanup(working_copy)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
865
  security.declareProtected('Import/Export objects', 'remove')
866
  # path can be a list or not (relative or absolute)
Christophe Dumez's avatar
Christophe Dumez committed
867
  def remove(self, path, business_template=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
868 869
    """Remove a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
870
    if business_template is not None:
871
      if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
872 873
        path = [self._getWorkingPath(self\
        .relativeToAbsolute(x, business_template)) for x in path]
874
      else:
Christophe Dumez's avatar
Christophe Dumez committed
875 876
        path = self._getWorkingPath(self.relativeToAbsolute(path, \
        business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
877
    client = self._getClient()
878
    return client.remove(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
879 880 881 882 883 884

  security.declareProtected('Import/Export objects', 'move')
  def move(self, src, dest):
    """Move/Rename a file or a directory.
    """
    client = self._getClient()
885
    return client.move(self._getWorkingPath(src), self._getWorkingPath(dest))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
886

Christophe Dumez's avatar
Christophe Dumez committed
887
  security.declareProtected('Import/Export objects', 'ls')
888
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
889
  def ls(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
890 891 892
    """Display infos about a file.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
893 894
    return client.ls(self._getWorkingPath(self\
    .relativeToAbsolute(path, business_template)))
Christophe Dumez's avatar
Christophe Dumez committed
895

Yoshinori Okuji's avatar
Yoshinori Okuji committed
896
  security.declareProtected('Import/Export objects', 'diff')
897
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
898
  def diff(self, path, business_template, revision1=None, revision2=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
899 900 901
    """Make a diff for a file or a directory.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
902 903
    return client.diff(self._getWorkingPath(self.relativeToAbsolute(path, \
    business_template)), revision1, revision2)
904
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
905
  security.declareProtected('Import/Export objects', 'revert')
906
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
907
  def revert(self, path, business_template=None, recurse=False):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
908 909 910
    """Revert local changes in a file or a directory.
    """
    client = self._getClient()
911
    if not isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
912 913 914 915 916
      path = [self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))]
    if business_template is not None:
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
Christophe Dumez's avatar
Christophe Dumez committed
917
    client.revert(path, recurse)
918 919 920

  security.declareProtected('Import/Export objects', 'revertZODB')
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
921 922
  def revertZODB(self, business_template, added_files=None, \
  other_files=None, recurse=False):
923 924 925 926 927
    """Revert local changes in a file or a directory
       in ZODB and on hard drive
    """
    client = self._getClient()
    object_to_update = {}
Christophe Dumez's avatar
Christophe Dumez committed
928
    # Transform params to list if they are not already lists
929 930 931 932 933
    if not added_files :
      added_files = []
    if not other_files :
      other_files = []
    if not isinstance(added_files, list) :
Christophe Dumez's avatar
Christophe Dumez committed
934
      added_files = [added_files]
935
    if not isinstance(other_files, list) :
Christophe Dumez's avatar
Christophe Dumez committed
936
      other_files = [other_files]
937 938
    
    # Reinstall removed or modified files
Christophe Dumez's avatar
Christophe Dumez committed
939 940
    for path in other_files :
      path_list = self._getWorkingPath(path).split(os.sep)
941 942 943 944
      if 'bt' not in path_list:
        if len(path_list) > 2 :
          tmp = os.sep.join(path_list[2:])
          # Remove file extension
Christophe Dumez's avatar
Christophe Dumez committed
945
          tmp = os.path.splitext(tmp)[0]
946
          object_to_update[tmp] = 'install'
947
    path_added_list = []
948
    # remove added files
Christophe Dumez's avatar
Christophe Dumez committed
949 950
    for path in added_files :
      path_list = self._getWorkingPath(path).split(os.sep)
951 952 953 954
      if 'bt' not in path_list:
        if len(path_list) > 2 :
          tmp = os.sep.join(path_list[2:])
          # Remove file extension
Christophe Dumez's avatar
Christophe Dumez committed
955 956
          tmp = os.path.splitext(tmp)[0]
          path_added_list.append(tmp)
957 958
    ## hack to remove objects
    # Create a temporary bt with objects to delete
Christophe Dumez's avatar
Christophe Dumez committed
959 960
    tmp_bt = getToolByName(business_template, 'portal_templates')\
    .newContent(portal_type="Business Template")
961 962 963 964 965 966 967 968 969
    tmp_bt.setTemplatePathList(path_added_list)
    tmp_bt.setTitle('tmp_bt_revert')
    # Build bt
    tmp_bt.edit()
    tmp_bt.build()
    # Install then uninstall it to remove objects from ZODB
    tmp_bt.install()
    tmp_bt.uninstall()
    # Remove it from portal template
Christophe Dumez's avatar
Christophe Dumez committed
970
    business_template.portal_templates.manage_delObjects(ids=tmp_bt.getId())
971 972
    #revert changes
    added_files.extend(other_files)
Christophe Dumez's avatar
Christophe Dumez committed
973 974
    to_revert = [self.relativeToAbsolute(x, business_template) \
    for x in added_files]
975 976 977
    if len(to_revert) != 0 :
      client.revert(to_revert, recurse)
      # Partially reinstall installed bt
Christophe Dumez's avatar
Christophe Dumez committed
978 979
      installed_bt = business_template.portal_templates\
      .getInstalledBusinessTemplate(business_template.getTitle())
980
      installed_bt.reinstall(object_to_update=object_to_update, force=0)
Christophe Dumez's avatar
Christophe Dumez committed
981 982 983
    
  security.declareProtected('Import/Export objects', 'resolved')
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
984
  def resolved(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
985 986 987 988
    """remove conflicted status
    """
    client = self._getClient()
    if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
989 990
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
Christophe Dumez's avatar
Christophe Dumez committed
991
    else:
Christophe Dumez's avatar
Christophe Dumez committed
992 993
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
Christophe Dumez's avatar
Christophe Dumez committed
994
    return client.resolved(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
995

Christophe Dumez's avatar
Christophe Dumez committed
996 997 998 999
  def relativeToAbsolute(self, path, business_template):
    """ Return an absolute path given the absolute one
        and the business template
    """
1000 1001 1002 1003
    if path[0] == os.sep:
      # already absolute
      return path
    # relative path
Christophe Dumez's avatar
Christophe Dumez committed
1004 1005 1006
    if path.split(os.sep)[0] == business_template.getTitle():
      return os.path.join(self.getSubversionPath(business_template, \
      False), path)
1007
    else:
Christophe Dumez's avatar
Christophe Dumez committed
1008
      return os.path.join(self.getSubversionPath(business_template), path)
1009

Yoshinori Okuji's avatar
Yoshinori Okuji committed
1010
  security.declareProtected('Import/Export objects', 'checkin')
1011
  # path can be relative or absolute (can be a list of paths too)
Christophe Dumez's avatar
Christophe Dumez committed
1012
  def checkin(self, path, business_template, log_message=None, recurse=True):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1013 1014
    """Commit local changes.
    """
1015
    if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
1016 1017
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
1018
    else:
Christophe Dumez's avatar
Christophe Dumez committed
1019 1020
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
1021
    client = self._getClient()
1022
    return client.checkin(path, log_message, recurse)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1023

1024
  security.declareProtected('Import/Export objects', 'getLastChangelog')
Christophe Dumez's avatar
Christophe Dumez committed
1025
  def getLastChangelog(self, business_template):
1026 1027
    """Return last changelog of a business template
    """
Christophe Dumez's avatar
Christophe Dumez committed
1028
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template))
1029
    changelog_path = bt_path + os.sep + 'bt' + os.sep + 'change_log'
Christophe Dumez's avatar
Christophe Dumez committed
1030
    changelog = ""
1031 1032 1033 1034 1035 1036 1037
    if os.path.exists(changelog_path):
      changelog_file = open(changelog_path, 'r')
      changelog_lines = changelog_file.readlines()
      changelog_file.close()
      for line in changelog_lines:
        if line.strip() == '':
          break
Christophe Dumez's avatar
Christophe Dumez committed
1038
        changelog += line
1039 1040 1041
    return changelog
    

Yoshinori Okuji's avatar
Yoshinori Okuji committed
1042 1043 1044 1045 1046
  security.declareProtected('Import/Export objects', 'status')
  def status(self, path, **kw):
    """Get status.
    """
    client = self._getClient()
1047
    return client.status(self._getWorkingPath(path), **kw)
1048
  
1049 1050 1051 1052 1053
  security.declareProtected('Import/Export objects', 'unversionedFiles')
  def unversionedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
1054
    status_list = client.status(self._getWorkingPath(path), **kw)
1055
    unversioned_list = []
Christophe Dumez's avatar
Christophe Dumez committed
1056 1057
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "unversioned":
1058
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
1059
        my_dict['uid'] = status_obj.getPath()
1060 1061 1062
        unversioned_list.append(my_dict)
    return unversioned_list
      
1063 1064 1065 1066 1067
  security.declareProtected('Import/Export objects', 'conflictedFiles')
  def conflictedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
1068
    status_list = client.status(self._getWorkingPath(path), **kw)
1069
    conflicted_list = []
Christophe Dumez's avatar
Christophe Dumez committed
1070 1071
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "conflicted":
1072
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
1073
        my_dict['uid'] = status_obj.getPath()
1074 1075 1076
        conflicted_list.append(my_dict)
    return conflicted_list

1077
  security.declareProtected('Import/Export objects', 'removeAllInList')
Christophe Dumez's avatar
Christophe Dumez committed
1078
  def removeAllInList(self, path_list):
1079 1080
    """Remove all files and folders in list
    """
Christophe Dumez's avatar
Christophe Dumez committed
1081 1082
    for file_path in path_list:
      removeAll(file_path)
1083
    
Christophe Dumez's avatar
Christophe Dumez committed
1084 1085 1086
  def getModifiedTree(self, business_template, show_unmodified=False) :
    """ Return tree of files returned by svn status
    """
1087
    # Get subversion path without business template name at the end
Christophe Dumez's avatar
Christophe Dumez committed
1088 1089
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template, \
    False))
1090 1091 1092
    if bt_path[-1] != '/':
      bt_path += '/'
    # Business template root directory is the root of the tree
Christophe Dumez's avatar
Christophe Dumez committed
1093 1094
    root = Dir(business_template.getTitle(), "normal")
    something_modified = False
1095
    
1096
    # We browse the files returned by svn status
Christophe Dumez's avatar
Christophe Dumez committed
1097 1098
    for status_obj in self.status(os.path.join(bt_path, \
    business_template.getTitle())) :
1099
      # can be (normal, added, modified, deleted, conflicted, unversioned)
1100 1101
      status = str(status_obj.getTextStatus())
      if (show_unmodified or status != "normal") and status != "unversioned":
Christophe Dumez's avatar
Christophe Dumez committed
1102
        something_modified = True
1103 1104 1105 1106 1107 1108
        # Get object path
        full_path = status_obj.getPath()
        relative_path = full_path.replace(bt_path, '')
        filename = os.path.basename(relative_path)

        # Always start from root
1109
        parent = root
Christophe Dumez's avatar
Christophe Dumez committed
1110
        
1111 1112
        # First we add the directories present in the path to the tree
        # if it does not already exist
Christophe Dumez's avatar
Christophe Dumez committed
1113 1114 1115 1116 1117
        for directory in relative_path.split(os.sep)[1:-1] :
          if directory :
            if directory not in parent.getSubDirsNameList() :
              parent.sub_dirs.append(Dir(directory, "normal"))
            parent = parent.getDirFromName(directory)
1118 1119 1120
        
        # Consider the whole path which can be a folder or a file
        # We add it the to the tree if it does not already exist
Christophe Dumez's avatar
Christophe Dumez committed
1121
        if os.path.isdir(full_path) :
1122 1123 1124 1125 1126
          if filename == parent.name :
            parent.status = status
          elif filename not in parent.getSubDirsNameList() :
            # Add new dir to the tree
            parent.sub_dirs.append(Dir(filename, str(status)))
Christophe Dumez's avatar
Christophe Dumez committed
1127
          else :
1128 1129 1130
            # update msg status
            tmp = parent.getDirFromName(filename)
            tmp.status = str(status)
Christophe Dumez's avatar
Christophe Dumez committed
1131
        else :
1132 1133
          # add new file to the tree
          parent.sub_files.append(File(filename, str(status)))
Christophe Dumez's avatar
Christophe Dumez committed
1134
    return something_modified and root
1135
  
Christophe Dumez's avatar
Christophe Dumez committed
1136 1137 1138 1139 1140 1141 1142 1143 1144
  def extractBT(self, business_template):
    """ 
     Extract business template to hard drive
     and do svn add/del stuff comparing it
     to local working copy
    """
    business_template.build()
    svn_path = self._getWorkingPath(self.getSubversionPath(business_template) \
    + os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
1145
    path = mktemp() + os.sep
1146
    try:
Christophe Dumez's avatar
Christophe Dumez committed
1147
      business_template.export(path=path, local=1)
1148
      # svn del deleted files
Christophe Dumez's avatar
Christophe Dumez committed
1149
      self.deleteOldFiles(svn_path, path)
1150
      # add new files and copy
Christophe Dumez's avatar
Christophe Dumez committed
1151 1152 1153 1154
      self.addNewFiles(svn_path, path)
      self.goToWorkingCopy(business_template)
    except (pysvn.ClientError, NotFound, AttributeError, \
    AttributeError, Error), error:
1155
      # Clean up
1156
      removeAll(path)
1157
      raise error
1158
    # Clean up
Christophe Dumez's avatar
Christophe Dumez committed
1159
    self.activate().removeAllInList([path, ])
1160
    
Christophe Dumez's avatar
Christophe Dumez committed
1161 1162 1163 1164 1165 1166 1167
  def importBT(self, business_template):
    """
     Import business template from local
     working copy
    """
    return business_template.download(self._getWorkingPath(self\
    .getSubversionPath(business_template)))
1168
    
1169 1170
  # Get a list of files and keep only parents
  # Necessary before recursively commit removals
Christophe Dumez's avatar
Christophe Dumez committed
1171 1172 1173 1174 1175 1176 1177 1178
  def cleanChildrenInList(self, path_list):
    """
     Get a list of files and keep only parents
     Necessary before recursively commit removals
    """
    res = path_list
    for file_path in path_list:
      res = [x for x in res if file_path == x or file_path not in x]
1179
    return res
1180

1181 1182
  # return a set with directories present in the directory
  def getSetDirsForDir(self, directory):
1183
    dir_set = set()
Christophe Dumez's avatar
Christophe Dumez committed
1184
    for root, dirs, _ in cacheWalk(directory):
1185 1186 1187 1188 1189
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
      # get Directories
      for name in dirs:
1190
        i = root.replace(directory, '').count(os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
1191 1192
        path = os.path.join(root, name)
        dir_set.add((i, path.replace(directory,'')))
1193 1194 1195 1196 1197
    return dir_set
      
  # return a set with files present in the directory
  def getSetFilesForDir(self, directory):
    dir_set = set()
1198
    for root, dirs, files in cacheWalk(directory):
1199 1200 1201
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
1202
      # get Files
1203 1204
      for name in files:
        i = root.replace(directory, '').count(os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
1205 1206
        path = os.path.join(root, name)
        dir_set.add((i, path.replace(directory,'')))
1207
    return dir_set
1208
  
1209
  # return files present in new_dir but not in old_dir
1210 1211
  # return a set of relative paths
  def getNewFiles(self, old_dir, new_dir):
1212 1213 1214 1215
    if old_dir[-1] != os.sep:
      old_dir += os.sep
    if new_dir[-1] != os.sep:
      new_dir += os.sep
1216 1217
    old_set = self.getSetFilesForDir(old_dir)
    new_set = self.getSetFilesForDir(new_dir)
1218 1219
    return new_set.difference(old_set)

1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230
  # return dirs present in new_dir but not in old_dir
  # return a set of relative paths
  def getNewDirs(self, old_dir, new_dir):
    if old_dir[-1] != os.sep:
      old_dir += os.sep
    if new_dir[-1] != os.sep:
      new_dir += os.sep
    old_set = self.getSetDirsForDir(old_dir)
    new_set = self.getSetDirsForDir(new_dir)
    return new_set.difference(old_set)
    
Christophe Dumez's avatar
Christophe Dumez committed
1231 1232 1233
  def deleteOldFiles(self, old_dir, new_dir):
    """ svn del files that have been removed in new dir
    """
1234
    # detect removed files
1235
    files_set = self.getNewFiles(new_dir, old_dir)
1236 1237
    # detect removed directories
    dirs_set = self.getNewDirs(new_dir, old_dir)
1238
    # svn del
Christophe Dumez's avatar
Christophe Dumez committed
1239 1240 1241 1242 1243 1244
    path_list = [x for x in files_set]
    path_list.sort()
    self.remove([os.path.join(old_dir, x[1]) for x in path_list])
    path_list = [x for x in dirs_set]
    path_list.sort()
    self.remove([os.path.join(old_dir, x[1]) for x in path_list])
1245
  
Christophe Dumez's avatar
Christophe Dumez committed
1246 1247 1248
  def addNewFiles(self, old_dir, new_dir):
    """ copy files and add new files
    """
1249
    # detect created files
1250
    files_set = self.getNewFiles(old_dir, new_dir)
1251 1252
    # detect created directories
    dirs_set = self.getNewDirs(old_dir, new_dir)
1253
    # Copy files
1254
    copytree(new_dir, old_dir)
1255
    # svn add
Christophe Dumez's avatar
Christophe Dumez committed
1256 1257 1258 1259 1260 1261
    path_list = [x for x in dirs_set]
    path_list.sort()
    self.add([os.path.join(old_dir, x[1]) for x in path_list])
    path_list = [x for x in files_set]
    path_list.sort()
    self.add([os.path.join(old_dir, x[1]) for x in path_list])
1262
  
Christophe Dumez's avatar
Christophe Dumez committed
1263 1264 1265
  def treeToXML(self, item, business_template) :
    """ Convert tree in memory to XML
    """
1266 1267
    output = "<?xml version='1.0' encoding='iso-8859-1'?>"+ os.linesep
    output += "<tree id='0'>" + os.linesep
Christophe Dumez's avatar
Christophe Dumez committed
1268
    output = self._treeToXML(item, output, business_template.getTitle(), True)
1269 1270
    output += "</tree>" + os.linesep
    return output
1271
  
1272
  def _treeToXML(self, item, output, relative_path, first) :
Christophe Dumez's avatar
Christophe Dumez committed
1273 1274 1275 1276
    """
     Private function to convert recursively tree 
     in memory to XML
    """
1277
    # Choosing a color coresponding to the status
1278 1279 1280 1281 1282 1283 1284 1285 1286
    status = item.status
    if status == 'added' :
      color = 'green'
    elif status == 'modified' or  status == 'replaced' :
      color = 'orange'
    elif status == 'deleted' :
      color = 'red'
    elif status == 'conflicted' :
      color = 'grey'
Christophe Dumez's avatar
Christophe Dumez committed
1287
    else :
1288
      color = 'black'
1289
    if isinstance(item, Dir) :
Christophe Dumez's avatar
Christophe Dumez committed
1290
      if first :
1291
        output += '<item open="1" text="%s" id="%s" aCol="%s" '\
Christophe Dumez's avatar
Christophe Dumez committed
1292
        'im0="folder.png" im1="folder_open.png" '\
1293
        'im2="folder.png">'%(item.name, relative_path, color) + os.linesep
1294
        first = False
Christophe Dumez's avatar
Christophe Dumez committed
1295
      else :
1296
        output += '<item text="%s" id="%s" aCol="%s" im0="folder.png" ' \
1297 1298 1299
        'im1="folder_open.png" im2="folder.png">'%(item.name,
        relative_path, color) + os.linesep
      for it in item.getContent():
Christophe Dumez's avatar
Christophe Dumez committed
1300 1301
        output = self._treeToXML(item.getObjectFromName(it.name), output, \
        os.path.join(relative_path,it.name),first)
1302
      output += '</item>' + os.linesep
1303
    else :
1304
      output += '<item text="%s" id="%s" aCol="%s" im0="document.png"/>'\
1305
                %(item.name, relative_path, color) + os.linesep
1306
    return output
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1307 1308
    
InitializeClass(SubversionTool)