SubversionTool.py 45.2 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
from AccessControl import ClassSecurityInfo
from Globals import InitializeClass, DTMLFile
Jean-Paul Smets's avatar
Jean-Paul Smets committed
34
from Products.ERP5Type.Core.Folder import Folder
Yoshinori Okuji's avatar
Yoshinori Okuji committed
35 36 37
from Products.ERP5Type import Permissions
from Products.ERP5Subversion import _dtmldir
from Products.ERP5Subversion.SubversionClient import newSubversionClient
38
import os, re
Yoshinori Okuji's avatar
Yoshinori Okuji committed
39 40 41
from DateTime import DateTime
from cPickle import dumps, loads
from App.config import getConfiguration
42
from tempfile import gettempdir, mktemp
43
from Products.CMFCore.utils import getToolByName
44
import shutil
45
from xml.sax.saxutils import escape
46
from dircache import listdir
47
from OFS.Traversable import NotFound
48
from Products.ERP5Type.patches.copyTree import copytree, Error
49
from Products.ERP5Type.patches.cacheWalk import cacheWalk
50
from time import ctime
Aurel's avatar
Aurel committed
51

52 53 54 55 56
try:
  import pysvn
except ImportError:
  pysvn = None

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

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

71
class File(object):
Christophe Dumez's avatar
Christophe Dumez committed
72 73
  """ Class that represents a file in memory
  """
74 75 76 77 78 79 80 81
  __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
82 83 84
  """ Class that reprensents a folder in memory
  """
  __slots__ = ('status', 'name', 'sub_dirs', 'sub_files')
85 86 87 88 89 90 91 92
  # 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
93 94
    """ return a list of sub directories' names
    """
95 96 97
    return [d.name for d in self.sub_dirs]

  def getDirFromName(self, name):
Christophe Dumez's avatar
Christophe Dumez committed
98 99 100 101 102
    """ return directory in subdirs given its name
    """
    for directory in self.sub_dirs:
      if directory.name == name:
        return directory
103 104
      
  def getObjectFromName(self, name):
Christophe Dumez's avatar
Christophe Dumez committed
105 106 107 108 109 110 111 112
    """ 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
113 114
      
  def getContent(self):
Christophe Dumez's avatar
Christophe Dumez committed
115 116
    """ return content for directory
    """
117 118 119 120
    content = self.sub_dirs
    content.extend(self.sub_files)
    return content
## End of Dir Class
121

122 123 124 125 126 127 128 129 130
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
131 132

class SubversionNotAWorkingCopyError(Exception):
133
  """The base exception class when directory is not a working copy
134 135
  """
  pass
136

137 138 139 140 141
class SubversionConflictError(Exception):
  """The base exception class when there is a conflict
  """
  pass

142 143
class SubversionSecurityError(Exception): pass

144 145 146 147 148
class SubversionBusinessTemplateNotInstalled(Exception):
  """ Exception called when the business template is not installed
  """
  pass

149 150 151 152 153
class UnauthorizedAccessToPath(Exception):
  """ When path is not in zope home instance
  """
  pass

154
    
155 156 157 158 159 160 161 162 163 164 165 166 167 168
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:
169
    color = '#a1559a' #light purple
170 171 172
  elif 'value' in text:
    color = 'purple'
  elif 'key' in text:
173
    color = '#0c4f0c'#dark green
174
  else:
Christophe Dumez's avatar
Christophe Dumez committed
175
    color = 'blue'
176
  return "<span style='color: %s'>%s</span>" % (color, text, )
177 178 179 180 181 182
    
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
183 184
  html = html.replace(' ', NBSP)
  html = html.replace('\t', NBSP_TAB)
185
  # Colorize comments
Christophe Dumez's avatar
Christophe Dumez committed
186 187
  pattern = re.compile(r'#.*')
  html = pattern.sub(colorizeTag, html)
188
  # Colorize tags
Christophe Dumez's avatar
Christophe Dumez committed
189 190
  pattern = re.compile(r'&lt;.*?&gt;')
  html = pattern.sub(colorizeTag, html)
191
  # Colorize strings
Christophe Dumez's avatar
Christophe Dumez committed
192 193
  pattern = re.compile(r'\".*?\"')
  html = pattern.sub(colorizeTag, html)
Christophe Dumez's avatar
Christophe Dumez committed
194
  html = html.replace(os.linesep, os.linesep+"<br/>")
195
  return html
196 197

class DiffFile:
Christophe Dumez's avatar
Christophe Dumez committed
198
  """
199
  # Members :
Christophe Dumez's avatar
Christophe Dumez committed
200 201 202 203 204
   - path : path of the modified file
   - children : sub codes modified
   - old_revision
   - new_revision
  """
205

206
  def __init__(self, raw_diff):
207
    if '@@' not in raw_diff:
Christophe Dumez's avatar
Christophe Dumez committed
208
      self.binary = True
209 210
      return
    else:
Christophe Dumez's avatar
Christophe Dumez committed
211
      self.binary = False
212
    self.header = raw_diff.split('@@')[0][:-1]
213
    # Getting file path in header
214
    self.path = self.header.split('====')[0][:-1].strip()
215
    # Getting revisions in header
216
    for line in self.header.splitlines():
217
      if line.startswith('--- '):
218
        tmp = re.search('\\([^)]+\\)$', line)
219
        self.old_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip()
220
      if line.startswith('+++ '):
221
        tmp = re.search('\\([^)]+\\)$', line)
222
        self.new_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip()
223
    # Splitting the body from the header
224
    self.body = os.linesep.join(raw_diff.strip().splitlines()[4:])
225
    # Now splitting modifications
226
    self.children = []
227 228
    first = True
    tmp = []
229
    for line in self.body.splitlines():
230 231
      if line:
        if line.startswith('@@') and not first:
232
          self.children.append(CodeBlock(os.linesep.join(tmp)))
Christophe Dumez's avatar
Christophe Dumez committed
233
          tmp = [line, ]
234 235 236
        else:
          first = False
          tmp.append(line)
237
    self.children.append(CodeBlock(os.linesep.join(tmp)))
238
    
239
  def toHTML(self):
Christophe Dumez's avatar
Christophe Dumez committed
240 241
    """ return HTML diff
    """
242
    # Adding header of the table
243
    if self.binary:
Christophe Dumez's avatar
Christophe Dumez committed
244
      return '<b>Folder or binary file or just no changes!</b><br/><br/><br/>'
245
    
Christophe Dumez's avatar
Christophe Dumez committed
246 247
    html_list = []
    html_list.append('''
248
    <table style="text-align: left; width: 100%%; border: 0;" cellpadding="0" cellspacing="0">
249
  <tbody>
250 251 252 253
    <tr>
      <td style="background-color: grey; text-align: center; font-weight: bold;">%s</td>
      <td style="background-color: black; width: 2px;"></td>
      <td style="background-color: grey; text-align: center; font-weight: bold;">%s</td>
Christophe Dumez's avatar
Christophe Dumez committed
254
    </tr>''' % (self.old_revision, self.new_revision))
Christophe Dumez's avatar
Christophe Dumez committed
255
    header_color = 'grey'
256 257 258 259 260 261
    child_html_text = '''<tr><td style="background-color: %(headcolor)s">
    &nbsp;</td><td style="background-color: black; width: 2px;"></td>
    <td style="background-color: %(headcolor)s">&nbsp;</td></tr><tr>
    <td style="background-color: rgb(68, 132, 255);font-weight: bold;">Line %(oldline)s</td>
    <td style="background-color: black; width: 2px;"></td>
    <td style="background-color: rgb(68, 132, 255);font-weight: bold;">Line %(newline)s</td>
Christophe Dumez's avatar
Christophe Dumez committed
262
    </tr>'''
263
    for child in self.children:
264
      # Adding line number of the modification
Christophe Dumez's avatar
Christophe Dumez committed
265
      html_list.append( child_html_text % {'headcolor':header_color, 'oldline':child.old_line, 'newline':child.new_line} )
Christophe Dumez's avatar
Christophe Dumez committed
266
      header_color = 'white'
267 268 269
      # Adding diff of the modification
      old_code_list = child.getOldCodeList()
      new_code_list = child.getNewCodeList()
Christophe Dumez's avatar
Christophe Dumez committed
270
      i = 0
271 272
      for old_line_tuple in old_code_list:
        new_line_tuple = new_code_list[i]
Christophe Dumez's avatar
Christophe Dumez committed
273 274
        new_line = new_line_tuple[0] or ' '
        old_line = old_line_tuple[0] or ' '
Christophe Dumez's avatar
Christophe Dumez committed
275
        i += 1
276
        html_list.append( '''<tr>
Christophe Dumez's avatar
Christophe Dumez committed
277
        <td style="background-color: %s">%s</td>
278
        <td style="background-color: black; width: 2px;"></td>
Christophe Dumez's avatar
Christophe Dumez committed
279
        <td style="background-color: %s">%s</td>
Christophe Dumez's avatar
Christophe Dumez committed
280 281 282 283 284
        </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))
        )
285
    html_list.append('''</tbody></table><br/>''')
Christophe Dumez's avatar
Christophe Dumez committed
286
    return '\n'.join(html_list)
287 288 289
      

class CodeBlock:
Christophe Dumez's avatar
Christophe Dumez committed
290 291 292 293 294 295 296 297 298 299 300
  """
   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)
  """
301

302
  def __init__(self, raw_diff):
303
    # Splitting body and header
304 305
    self.body = os.linesep.join(raw_diff.splitlines()[1:])
    self.header = raw_diff.splitlines()[0]
306
    # Getting modifications lines
307 308
    tmp = re.search('^@@ -\d+', self.header)
    self.old_line = tmp.string[tmp.start():tmp.end()][4:]
Christophe Dumez's avatar
Christophe Dumez committed
309 310
    tmp = re.search('\+\d+', self.header)
    self.new_line = tmp.string[tmp.start():tmp.end()][1:]
311 312
    # Splitting modifications in SubCodeBlocks
    in_modif = False
313
    self.children = []
Christophe Dumez's avatar
Christophe Dumez committed
314
    tmp = []
315
    for line in self.body.splitlines():
316 317 318 319 320
      if line:
        if (line.startswith('+') or line.startswith('-')):
          if in_modif:
            tmp.append(line)
          else:
321
            self.children.append(SubCodeBlock(os.linesep.join(tmp)))
Christophe Dumez's avatar
Christophe Dumez committed
322
            tmp = [line, ]
323 324
            in_modif = True
        else:
Christophe Dumez's avatar
Christophe Dumez committed
325 326 327 328 329 330
          if in_modif:
            self.children.append(SubCodeBlock(os.linesep.join(tmp)))
            tmp = [line, ]
            in_modif = False
          else:
            tmp.append(line)
331
    self.children.append(SubCodeBlock(os.linesep.join(tmp)))
332
    
333
  def getOldCodeList(self):
Christophe Dumez's avatar
Christophe Dumez committed
334 335
    """ Return code before modification
    """
336
    tmp = []
337
    for child in self.children:
338 339 340
      tmp.extend(child.getOldCodeList())
    return tmp
    
341
  def getNewCodeList(self):
Christophe Dumez's avatar
Christophe Dumez committed
342 343
    """ Return code after modification
    """
344
    tmp = []
345
    for child in self.children:
346 347 348 349
      tmp.extend(child.getNewCodeList())
    return tmp
    
class SubCodeBlock:
Christophe Dumez's avatar
Christophe Dumez committed
350 351
  """ a SubCodeBlock contain 0 or 1 modification (not more)
  """
352
  def __init__(self, code):
353 354
    self.body = code
    self.modification = self._getModif()
Christophe Dumez's avatar
Christophe Dumez committed
355 356
    self.old_code_length = self._getOldCodeLength()
    self.new_code_length = self._getNewCodeLength()
357
    # Choosing background color
358 359 360 361 362 363
    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
364
    else: # addition
365
      self.color = 'rgb(83, 253, 74);'#light green
366
    
367
  def _getModif(self):
Christophe Dumez's avatar
Christophe Dumez committed
368 369 370
    """ Return type of modification :
        addition, deletion, none
    """
371 372
    nb_plus = 0
    nb_minus = 0
373
    for line in self.body.splitlines():
374
      if line.startswith("-"):
Christophe Dumez's avatar
Christophe Dumez committed
375
        nb_minus -= 1
376
      elif line.startswith("+"):
Christophe Dumez's avatar
Christophe Dumez committed
377 378
        nb_plus += 1
    if (nb_plus == 0 and nb_minus == 0):
379
      return 'none'
Christophe Dumez's avatar
Christophe Dumez committed
380
    if (nb_minus == 0):
Christophe Dumez's avatar
Christophe Dumez committed
381
      return 'addition'
Christophe Dumez's avatar
Christophe Dumez committed
382
    if (nb_plus == 0):
Christophe Dumez's avatar
Christophe Dumez committed
383
      return 'deletion'
384
    return 'change'
Christophe Dumez's avatar
Christophe Dumez committed
385 386
      
  def _getOldCodeLength(self):
Christophe Dumez's avatar
Christophe Dumez committed
387 388
    """ Private function to return old code length
    """
Christophe Dumez's avatar
Christophe Dumez committed
389
    nb_lines = 0
390
    for line in self.body.splitlines():
Christophe Dumez's avatar
Christophe Dumez committed
391
      if not line.startswith("+"):
Christophe Dumez's avatar
Christophe Dumez committed
392
        nb_lines += 1
Christophe Dumez's avatar
Christophe Dumez committed
393 394 395
    return nb_lines
      
  def _getNewCodeLength(self):
Christophe Dumez's avatar
Christophe Dumez committed
396 397
    """ Private function to return new code length
    """
Christophe Dumez's avatar
Christophe Dumez committed
398
    nb_lines = 0
399
    for line in self.body.splitlines():
Christophe Dumez's avatar
Christophe Dumez committed
400
      if not line.startswith("-"):
Christophe Dumez's avatar
Christophe Dumez committed
401
        nb_lines += 1
Christophe Dumez's avatar
Christophe Dumez committed
402
    return nb_lines
403
  
404
  def getOldCodeList(self):
Christophe Dumez's avatar
Christophe Dumez committed
405 406 407
    """ Return code before modification
    """
    if self.modification == 'none':
408
      old_code = [(x, 'white') for x in self.body.splitlines()]
Christophe Dumez's avatar
Christophe Dumez committed
409
    elif self.modification == 'change':
410
      old_code = [self._getOldCodeList(x) for x in self.body.splitlines() \
Christophe Dumez's avatar
Christophe Dumez committed
411
      if self._getOldCodeList(x)[0]]
Christophe Dumez's avatar
Christophe Dumez committed
412 413
      # 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
414 415
        filling = [(None, self.color)] * (self.new_code_length - \
        self.old_code_length)
Christophe Dumez's avatar
Christophe Dumez committed
416
        old_code.extend(filling)
417
    else: # deletion or addition
418
      old_code = [self._getOldCodeList(x) for x in self.body.splitlines()]
419
    return old_code
420
  
421
  def _getOldCodeList(self, line):
Christophe Dumez's avatar
Christophe Dumez committed
422 423
    """ Private function to return code before modification
    """
424
    if line.startswith('+'):
425
      return (None, self.color)
426
    if line.startswith('-'):
Christophe Dumez's avatar
Christophe Dumez committed
427
      return (' ' + line[1:], self.color)
428
    return (line, self.color)
429
  
430
  def getNewCodeList(self):
Christophe Dumez's avatar
Christophe Dumez committed
431 432 433
    """ Return code after modification
    """
    if self.modification == 'none':
434
      new_code = [(x, 'white') for x in self.body.splitlines()]
Christophe Dumez's avatar
Christophe Dumez committed
435
    elif self.modification == 'change':
436
      new_code = [self._getNewCodeList(x) for x in self.body.splitlines() \
Christophe Dumez's avatar
Christophe Dumez committed
437
      if self._getNewCodeList(x)[0]]
Christophe Dumez's avatar
Christophe Dumez committed
438 439
      # 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
440 441
        filling = [(None, self.color)] * (self.old_code_length - \
        self.new_code_length)
Christophe Dumez's avatar
Christophe Dumez committed
442
        new_code.extend(filling)
443
    else: # deletion or addition
444
      new_code = [self._getNewCodeList(x) for x in self.body.splitlines()]
445
    return new_code
446
  
447
  def _getNewCodeList(self, line):
Christophe Dumez's avatar
Christophe Dumez committed
448 449
    """ Private function to return code after modification
    """
450
    if line.startswith('-'):
451
      return (None, self.color)
452
    if line.startswith('+'):
Christophe Dumez's avatar
Christophe Dumez committed
453
      return (' ' + line[1:], self.color)
454
    return (line, self.color)
455
  
456
class SubversionTool(BaseTool, UniqueObject, Folder):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
457 458 459 460 461 462 463 464 465
  """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'
466 467 468
  
  top_working_path = getConfiguration().instancehome
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
  # 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
488 489
    return Folder.__init__(self, SubversionTool.id)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
490 491

  def filtered_meta_types(self, user=None):
Christophe Dumez's avatar
Christophe Dumez committed
492 493 494 495 496 497 498 499 500 501
    """
     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
502
    
Christophe Dumez's avatar
Christophe Dumez committed
503
  # path is the path in svn working copy
504 505
  # return edit_path in zodb to edit it
  # return '#' if no zodb path is found
Christophe Dumez's avatar
Christophe Dumez committed
506
  def editPath(self, business_template, path):
Christophe Dumez's avatar
Christophe Dumez committed
507
    """Return path to edit file
508
       path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
509
    """
Christophe Dumez's avatar
Christophe Dumez committed
510
    path = self.relativeToAbsolute(path, business_template).replace('\\', '/')
511
    if 'bt' in path.split('/'):
512
      # not in zodb
Christophe Dumez's avatar
Christophe Dumez committed
513
      return '#'
514 515 516
    # if file have been deleted then not in zodb
    if not os.path.exists(path):
      return '#'
Christophe Dumez's avatar
Christophe Dumez committed
517
    svn_path = self.getSubversionPath(business_template).replace('\\', '/')
Christophe Dumez's avatar
Christophe Dumez committed
518 519
    edit_path = path.replace(svn_path, '').strip()
    if edit_path == '':
520 521
      # not in zodb 
      return '#'
522
    if edit_path[0] == '/':
523
      edit_path = edit_path[1:]
Christophe Dumez's avatar
Christophe Dumez committed
524 525
    edit_path = '/'.join(edit_path.split('/')[1:]).strip()
    if edit_path == '':
526 527
      # not in zodb 
      return '#'
528
    # remove file extension
Christophe Dumez's avatar
Christophe Dumez committed
529 530
    edit_path = os.path.splitext(edit_path)[0]
    # Add beginning and end of url
Christophe Dumez's avatar
Christophe Dumez committed
531 532
    edit_path = os.path.join(business_template.REQUEST["BASE2"], \
    edit_path, 'manage_main')
Christophe Dumez's avatar
Christophe Dumez committed
533 534
    return edit_path
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
535
  def _encodeLogin(self, realm, user, password):
Christophe Dumez's avatar
Christophe Dumez committed
536 537
    """ Encode login information.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
538 539 540
    return b64encode(dumps((realm, user, password)))

  def _decodeLogin(self, login):
Christophe Dumez's avatar
Christophe Dumez committed
541 542
    """ Decode login information.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
543
    return loads(b64decode(login))
544
  
Christophe Dumez's avatar
Christophe Dumez committed
545 546 547 548 549
  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
550
    
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
  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)
566
    expires = (DateTime() + 1).toZone('GMT').rfc822()
567
    request.set(self.login_cookie_name, value)
Christophe Dumez's avatar
Christophe Dumez committed
568 569
    response.setCookie(self.login_cookie_name, value, path = '/', \
    expires = expires)
570

Yoshinori Okuji's avatar
Yoshinori Okuji committed
571 572 573 574 575 576 577 578 579
  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
580
      
Christophe Dumez's avatar
Christophe Dumez committed
581
  def getHeader(self, business_template, file_path):
582
    file_path = self._getWorkingPath(self.relativeToAbsolute(file_path, business_template))
583 584
    header = '<a style="font-weight: bold" href="BusinessTemplate_viewSvnShowFile?file=' + \
    file_path + '">' + file_path + '</a>'
Christophe Dumez's avatar
Christophe Dumez committed
585
    edit_path = self.editPath(business_template, file_path)
586
    if edit_path != '#':
587
      header += '&nbsp;&nbsp;<a href="'+self.editPath(business_template, \
588
      file_path) + '"><img src="imgs/edit.png" style="border: 0" alt="edit" /></a>'
589
    return header
Yoshinori Okuji's avatar
Yoshinori Okuji committed
590 591 592 593 594 595 596 597 598 599

  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
600
    trust_item_list, permanent = loads(b64decode(trust))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
601
    return dict(trust_item_list), permanent
602
  
603 604 605
  def getPreferredUsername(self):
    """return username in preferences if set of the current username
    """
Christophe Dumez's avatar
Christophe Dumez committed
606 607
    username = self.getPortalObject().portal_preferences\
    .getPreferredSubversionUserName()
608 609 610 611 612
    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
613 614 615 616 617
  def diffHTML(self, file_path, business_template, revision1=None, \
  revision2=None):
    """ Return HTML diff
    """
    raw_diff = self.diff(file_path, business_template, revision1, revision2)
618
    return DiffFile(raw_diff).toHTML()
Christophe Dumez's avatar
Christophe Dumez committed
619
  
Christophe Dumez's avatar
Christophe Dumez committed
620 621 622
  def fileHTML(self, business_template, file_path):
    """ Display a file content in HTML with syntax highlighting
    """
623
    file_path = self._getWorkingPath(self.relativeToAbsolute(file_path, business_template))
624 625
    if os.path.exists(file_path):
      if os.path.isdir(file_path):
626
        text = "<span style='font-weight: bold; color: black;'>"+file_path+"</span><hr/>"
627
        text += file_path +" is a folder!"
628
      else:
629
        input_file = open(file_path, 'rU')
630
        head = "<span style='font-weight: bold; color: black;'>"+file_path+'</span>  <a href="' + \
Christophe Dumez's avatar
Christophe Dumez committed
631
        self.editPath(business_template, file_path) + \
Jérome Perrin's avatar
Jérome Perrin committed
632
        '"><img src="ERP5Subversion_imgs/edit.png" style="border: 0" alt="edit" /></a><hr/>'
Christophe Dumez's avatar
Christophe Dumez committed
633
        text = head + colorize(input_file.read())
634
        input_file.close()
635 636
    else:
      # see if tmp file is here (svn deleted file)
Christophe Dumez's avatar
Christophe Dumez committed
637 638
      if file_path[-1] == os.sep:
        file_path = file_path[:-1]
639 640
      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
641 642
      tmp_path = os.path.join(tmp_path, '.svn', 'text-base', \
      filename+'.svn-base')
643
      if os.path.exists(tmp_path):
644
        input_file = open(tmp_path, 'rU')
645
        head = "<span style='font-weight: bold'>"+tmp_path+"</span> (svn temporary file)<hr/>"
Christophe Dumez's avatar
Christophe Dumez committed
646
        text = head + colorize(input_file.read())
647
        input_file.close()
648
      else : # does not exist
649
        text = "<span style='font-weight: bold'>"+file_path+"</span><hr/>"
650
        text += file_path +" does not exist!"
651
    return text
652
      
Yoshinori Okuji's avatar
Yoshinori Okuji committed
653 654 655 656 657 658 659 660 661
  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
662
      trust_list.append(cookie)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
663 664 665 666
    # Set the cookie.
    response = request.RESPONSE
    trust_list.append(self._encodeSSLTrust(trust_dict, permanent))
    value = ','.join(trust_list)
667
    expires = (DateTime() + 1).toZone('GMT').rfc822()
668
    request.set(self.ssl_trust_cookie_name, value)
Christophe Dumez's avatar
Christophe Dumez committed
669 670
    response.setCookie(self.ssl_trust_cookie_name, value, path = '/', \
    expires = expires)
Christophe Dumez's avatar
Christophe Dumez committed
671 672
    
  def acceptSSLPerm(self, trust_dict):
Christophe Dumez's avatar
Christophe Dumez committed
673 674
    """ Accept SSL server permanently
    """
Christophe Dumez's avatar
Christophe Dumez committed
675
    self.acceptSSLServer(self, trust_dict, True)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692

  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)
693 694
  
  security.declareProtected('Import/Export objects', 'getSubversionPath')
Christophe Dumez's avatar
Christophe Dumez committed
695 696 697 698 699 700 701 702 703 704 705
  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()
706
    if not wc_list or len(wc_list) == 0 :
Christophe Dumez's avatar
Christophe Dumez committed
707 708 709 710 711 712 713
      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 bt_name in listdir(working_copy) :
        wc_path = os.path.join(working_copy, bt_name)
714 715 716 717 718
        if os.path.isdir(wc_path):
          if with_name:
            return wc_path
          else:
            return os.sep.join(wc_path.split(os.sep)[:-1])
719
    raise SubversionUnknownBusinessTemplateError, \
720
        "Could not find '%s' at first level of working copies." % (bt_name, )
721 722

  def _getWorkingPath(self, path):
Christophe Dumez's avatar
Christophe Dumez committed
723 724
    """ Check if the given path is reachable (allowed)
    """
725 726 727
    real_path = os.path.abspath(path)
    if not real_path.startswith(self.top_working_path) and \
        not real_path.startswith(gettempdir()):
Alexandre Boeglin's avatar
Alexandre Boeglin committed
728 729
      raise UnauthorizedAccessToPath, 'Unauthorized access to path %s.' \
          'It is NOT in your Zope home instance.' % real_path
730
    return real_path
731
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
732
  security.declareProtected('Import/Export objects', 'update')
Christophe Dumez's avatar
Christophe Dumez committed
733
  def update(self, business_template):
734 735 736
    """
     Update a working copy / business template, 
     reverting all local changes
Yoshinori Okuji's avatar
Yoshinori Okuji committed
737
    """
Christophe Dumez's avatar
Christophe Dumez committed
738
    path = self._getWorkingPath(self.getSubversionPath(business_template))
739
    # First remove unversioned in working copy that could conflict
740
    self.removeAllInList([x['uid'] for x in self.unversionedFiles(path)])
Yoshinori Okuji's avatar
Yoshinori Okuji committed
741
    client = self._getClient()
742 743 744
    # Revert local changes in working copy first
    # to import a "pure" BT after update
    self.revert(path=path, recurse=True)
745
    # removed unversioned files due to former added files that were reverted
746
    self.removeAllInList([x['uid'] for x in self.unversionedFiles(path)])
747 748 749
    # Update from SVN
    client.update(path)
    # Import in zodb
Christophe Dumez's avatar
Christophe Dumez committed
750
    return self.importBT(business_template)
751 752 753 754 755
  
  security.declareProtected('Import/Export objects', 'updateKeep')
  def updateKeep(self, business_template):
    """
     Update a working copy / business template, 
Christophe Dumez's avatar
Christophe Dumez committed
756
     keeping all local changes (appart from revision)
757 758 759 760 761 762 763 764
    """
    path = self._getWorkingPath(self.getSubversionPath(business_template))
    client = self._getClient()
    # Revert revision file so that it does not conflict
    revision_path = os.path.join(path, 'bt', 'revision')
    if os.path.exists(revision_path):
      self.revert(path=revision_path, recurse=False)
    # remove unversioned files in working copy that could be annoying
765
    self.removeAllInList([x['uid'] for x in self.unversionedFiles(path)])
766 767 768 769 770 771 772 773
    # Update from SVN
    client.update(path)
    # Check if some files are conflicted to raise an exception
    conflicted_list = self.conflictedFiles(self.getSubversionPath(business_template))
    if len(conflicted_list) != 0:
      raise SubversionConflictError, 'The following files conflicts (%s), please resolve manually.' % repr(conflicted_list)
    # Import in zodb
    return self.importBT(business_template)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
774

775
  security.declareProtected('Import/Export objects', 'switch')
Christophe Dumez's avatar
Christophe Dumez committed
776
  def switch(self, business_template, url):
777 778
    """switch SVN repository for a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
779
    path = self._getWorkingPath(self.getSubversionPath(business_template))
780
    client = self._getClient()
781 782
    if url[-1] == '/' :
      url = url[:-1]
783
    # Update from SVN
784
    client.switch(path=path, url=url)
785
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
786
  security.declareProtected('Import/Export objects', 'add')
787
  # path can be a list or not (relative or absolute)
Christophe Dumez's avatar
Christophe Dumez committed
788
  def add(self, path, business_template=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
789 790
    """Add a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
791
    if business_template is not None:
792
      if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
793 794
        path = [self._getWorkingPath(self.relativeToAbsolute(x, \
        business_template)) for x in path]
795
      else:
Christophe Dumez's avatar
Christophe Dumez committed
796 797
        path = self._getWorkingPath(self.relativeToAbsolute(path, \
        business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
798
    client = self._getClient()
799
    return client.add(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
800

801
  security.declareProtected('Import/Export objects', 'info')
Christophe Dumez's avatar
Christophe Dumez committed
802
  def info(self, business_template):
803 804
    """return info of working copy
    """
Christophe Dumez's avatar
Christophe Dumez committed
805 806
    working_copy = self._getWorkingPath(self\
    .getSubversionPath(business_template))
807 808 809
    client = self._getClient()
    return client.info(working_copy)
  
Christophe Dumez's avatar
Christophe Dumez committed
810
  security.declareProtected('Import/Export objects', 'log')
811
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
812
  def log(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
813 814 815
    """return log of a file or dir
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
816 817
    return client.log(self._getWorkingPath(self\
    .relativeToAbsolute(path, business_template)))
Christophe Dumez's avatar
Christophe Dumez committed
818
  
819
  security.declareProtected('Import/Export objects', 'cleanup')
Christophe Dumez's avatar
Christophe Dumez committed
820
  def cleanup(self, business_template):
821 822
    """remove svn locks in working copy
    """
Christophe Dumez's avatar
Christophe Dumez committed
823 824
    working_copy = self._getWorkingPath(self\
    .getSubversionPath(business_template))
825 826 827
    client = self._getClient()
    return client.cleanup(working_copy)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
828
  security.declareProtected('Import/Export objects', 'remove')
829
  # path can be a list or not (relative or absolute)
Christophe Dumez's avatar
Christophe Dumez committed
830
  def remove(self, path, business_template=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
831 832
    """Remove a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
833
    if business_template is not None:
834
      if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
835 836
        path = [self._getWorkingPath(self\
        .relativeToAbsolute(x, business_template)) for x in path]
837
      else:
Christophe Dumez's avatar
Christophe Dumez committed
838 839
        path = self._getWorkingPath(self.relativeToAbsolute(path, \
        business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
840
    client = self._getClient()
841
    return client.remove(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
842 843 844 845 846 847

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

Christophe Dumez's avatar
Christophe Dumez committed
850
  security.declareProtected('Import/Export objects', 'ls')
851
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
852
  def ls(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
853 854 855
    """Display infos about a file.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
856 857
    return client.ls(self._getWorkingPath(self\
    .relativeToAbsolute(path, business_template)))
Christophe Dumez's avatar
Christophe Dumez committed
858

Yoshinori Okuji's avatar
Yoshinori Okuji committed
859
  security.declareProtected('Import/Export objects', 'diff')
860
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
861
  def diff(self, path, business_template, revision1=None, revision2=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
862 863 864
    """Make a diff for a file or a directory.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
865 866
    return client.diff(self._getWorkingPath(self.relativeToAbsolute(path, \
    business_template)), revision1, revision2)
867
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
868
  security.declareProtected('Import/Export objects', 'revert')
869
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
870
  def revert(self, path, business_template=None, recurse=False):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
871 872 873
    """Revert local changes in a file or a directory.
    """
    client = self._getClient()
874
    if not isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
875 876 877 878 879
      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
880
    client.revert(path, recurse)
881 882 883

  security.declareProtected('Import/Export objects', 'revertZODB')
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
884 885
  def revertZODB(self, business_template, added_files=None, \
  other_files=None, recurse=False):
886 887
    """Revert local changes in a file or a directory
       in ZODB and on hard drive
888 889 890 891

       XXX-JPS: naming of parameters is wrong. added_files
       should be added_file_list. Action: rename to added_file_list
       and provide compatibility for scripts.
892 893 894
    """
    client = self._getClient()
    object_to_update = {}
Christophe Dumez's avatar
Christophe Dumez committed
895
    # Transform params to list if they are not already lists
896 897 898 899 900
    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
901
      added_files = [added_files]
902
    if not isinstance(other_files, list) :
Christophe Dumez's avatar
Christophe Dumez committed
903
      other_files = [other_files]
904
      
905
    # Reinstall removed or modified files
Christophe Dumez's avatar
Christophe Dumez committed
906
    for path in other_files :
907 908 909
      # security check
      self._getWorkingPath(self.relativeToAbsolute(path, business_template))
      path_list = path.split(os.sep)
910 911 912 913
      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
914
          tmp = os.path.splitext(tmp)[0]
915
          object_to_update[tmp] = 'install'
916
    path_added_list = []
917
    # remove added files
Christophe Dumez's avatar
Christophe Dumez committed
918
    for path in added_files :
919 920 921
      # security check
      self._getWorkingPath(self.relativeToAbsolute(path, business_template))
      path_list = path.split(os.sep)
922 923 924 925
      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
926 927
          tmp = os.path.splitext(tmp)[0]
          path_added_list.append(tmp)
928 929
    ## hack to remove objects
    # Create a temporary bt with objects to delete
930
    tmp_bt = getToolByName(business_template.getPortalObject(), 'portal_templates')\
Christophe Dumez's avatar
Christophe Dumez committed
931
    .newContent(portal_type="Business Template")
932 933 934 935 936 937 938 939 940
    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
941
    business_template.portal_templates.manage_delObjects(ids=tmp_bt.getId())
942 943
    #revert changes
    added_files.extend(other_files)
Christophe Dumez's avatar
Christophe Dumez committed
944 945
    to_revert = [self.relativeToAbsolute(x, business_template) \
    for x in added_files]
946 947 948
    if len(to_revert) != 0 :
      client.revert(to_revert, recurse)
      # Partially reinstall installed bt
Christophe Dumez's avatar
Christophe Dumez committed
949 950
      installed_bt = business_template.portal_templates\
      .getInstalledBusinessTemplate(business_template.getTitle())
951 952
      if installed_bt is None:
        raise SubversionBusinessTemplateNotInstalled, "Revert won't work if the business template is not installed. Please install it first."
953
      installed_bt.reinstall(object_to_update=object_to_update, force=0)
Christophe Dumez's avatar
Christophe Dumez committed
954 955 956
    
  security.declareProtected('Import/Export objects', 'resolved')
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
957
  def resolved(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
958 959 960 961
    """remove conflicted status
    """
    client = self._getClient()
    if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
962 963
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
Christophe Dumez's avatar
Christophe Dumez committed
964
    else:
Christophe Dumez's avatar
Christophe Dumez committed
965 966
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
Christophe Dumez's avatar
Christophe Dumez committed
967
    return client.resolved(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
968

Christophe Dumez's avatar
Christophe Dumez committed
969 970 971 972
  def relativeToAbsolute(self, path, business_template):
    """ Return an absolute path given the absolute one
        and the business template
    """
973 974 975 976
    if path[0] == os.sep:
      # already absolute
      return path
    # relative path
Christophe Dumez's avatar
Christophe Dumez committed
977 978 979
    if path.split(os.sep)[0] == business_template.getTitle():
      return os.path.join(self.getSubversionPath(business_template, \
      False), path)
980
    else:
Christophe Dumez's avatar
Christophe Dumez committed
981
      return os.path.join(self.getSubversionPath(business_template), path)
982

Yoshinori Okuji's avatar
Yoshinori Okuji committed
983
  security.declareProtected('Import/Export objects', 'checkin')
984
  # path can be relative or absolute (can be a list of paths too)
Christophe Dumez's avatar
Christophe Dumez committed
985
  def checkin(self, path, business_template, log_message=None, recurse=True):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
986 987
    """Commit local changes.
    """
988
    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]
991
    else:
Christophe Dumez's avatar
Christophe Dumez committed
992 993
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
994
    client = self._getClient()
995 996 997
    # Pysvn wants unicode objects
    if isinstance(log_message, str):
      log_message = log_message.decode('utf8')
998
    return client.checkin(path, log_message, recurse)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
999

1000
  security.declareProtected('Import/Export objects', 'getLastChangelog')
Christophe Dumez's avatar
Christophe Dumez committed
1001
  def getLastChangelog(self, business_template):
1002 1003
    """Return last changelog of a business template
    """
Christophe Dumez's avatar
Christophe Dumez committed
1004
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template))
1005
    changelog_path = bt_path + os.sep + 'bt' + os.sep + 'change_log'
Christophe Dumez's avatar
Christophe Dumez committed
1006
    changelog = ""
1007 1008 1009 1010 1011 1012 1013
    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
1014
        changelog += line
1015 1016 1017
    return changelog
    

Yoshinori Okuji's avatar
Yoshinori Okuji committed
1018 1019 1020 1021 1022
  security.declareProtected('Import/Export objects', 'status')
  def status(self, path, **kw):
    """Get status.
    """
    client = self._getClient()
1023
    return client.status(self._getWorkingPath(path), **kw)
1024
  
1025 1026 1027 1028 1029
  security.declareProtected('Import/Export objects', 'unversionedFiles')
  def unversionedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
1030
    status_list = client.status(self._getWorkingPath(path), **kw)
1031
    unversioned_list = []
Christophe Dumez's avatar
Christophe Dumez committed
1032 1033
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "unversioned":
1034
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
1035
        my_dict['uid'] = status_obj.getPath()
1036 1037 1038
        unversioned_list.append(my_dict)
    return unversioned_list
      
1039 1040 1041 1042 1043
  security.declareProtected('Import/Export objects', 'conflictedFiles')
  def conflictedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
1044
    status_list = client.status(self._getWorkingPath(path), **kw)
1045
    conflicted_list = []
Christophe Dumez's avatar
Christophe Dumez committed
1046 1047
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "conflicted":
1048
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
1049
        my_dict['uid'] = status_obj.getPath()
1050 1051 1052
        conflicted_list.append(my_dict)
    return conflicted_list

1053
  security.declareProtected('Import/Export objects', 'removeAllInList')
1054
  def removeAllInList(self, path_list, REQUEST=None):
1055 1056
    """Remove all files and folders in list
    """
1057 1058 1059
    if REQUEST is not None:
      # Security hole fix
      raise SubversionSecurityError, 'You are not allowed to delete these files'
Christophe Dumez's avatar
Christophe Dumez committed
1060
    for file_path in path_list:
1061 1062
      real_path = self._getWorkingPath(file_path)
      if os.path.isdir(real_path):
1063
        shutil.rmtree(real_path)
1064 1065
      elif os.path.isfile(real_path):
        os.remove(real_path)
1066
    
Christophe Dumez's avatar
Christophe Dumez committed
1067 1068 1069
  def getModifiedTree(self, business_template, show_unmodified=False) :
    """ Return tree of files returned by svn status
    """
1070
    # Get subversion path without business template name at the end
Christophe Dumez's avatar
Christophe Dumez committed
1071 1072
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template, \
    False))
1073 1074
    if bt_path[-1] != os.sep:
      bt_path += os.sep
1075
    # Business template root directory is the root of the tree
Christophe Dumez's avatar
Christophe Dumez committed
1076 1077
    root = Dir(business_template.getTitle(), "normal")
    something_modified = False
1078
    
1079
    statusObj_list = self.status(os.path.join(bt_path, \
1080
    business_template.getTitle()), update=False)
1081
    # We browse the files returned by svn status
1082
    for status_obj in statusObj_list :
1083
      # can be (normal, added, modified, deleted, conflicted, unversioned)
1084
      status = str(status_obj.getTextStatus())
1085
      if str(status_obj.getReposTextStatus()) != 'none':
Jérome Perrin's avatar
Jérome Perrin committed
1086
        status = "outdated"
1087
      if (show_unmodified or status != "normal") and status != "unversioned":
Christophe Dumez's avatar
Christophe Dumez committed
1088
        something_modified = True
1089 1090 1091 1092 1093 1094
        # 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
1095
        parent = root
Christophe Dumez's avatar
Christophe Dumez committed
1096
        
1097 1098
        # 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
1099 1100 1101 1102 1103
        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)
1104 1105 1106
        
        # 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
1107
        if os.path.isdir(full_path) :
1108 1109 1110 1111 1112
          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
1113
          else :
1114 1115 1116
            # update msg status
            tmp = parent.getDirFromName(filename)
            tmp.status = str(status)
Christophe Dumez's avatar
Christophe Dumez committed
1117
        else :
1118 1119
          # add new file to the tree
          parent.sub_files.append(File(filename, str(status)))
Christophe Dumez's avatar
Christophe Dumez committed
1120
    return something_modified and root
1121
  
Christophe Dumez's avatar
Christophe Dumez committed
1122 1123 1124 1125 1126 1127
  def extractBT(self, business_template):
    """ 
     Extract business template to hard drive
     and do svn add/del stuff comparing it
     to local working copy
    """
1128 1129
    if business_template.getBuildingState() == 'draft':
      business_template.edit()
Christophe Dumez's avatar
Christophe Dumez committed
1130 1131 1132
    business_template.build()
    svn_path = self._getWorkingPath(self.getSubversionPath(business_template) \
    + os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
1133
    path = mktemp() + os.sep
1134
    try:
1135 1136
      # XXX: Big hack to make export work as expected.
      get_transaction().commit()
Christophe Dumez's avatar
Christophe Dumez committed
1137
      business_template.export(path=path, local=1)
1138
      # svn del deleted files
Christophe Dumez's avatar
Christophe Dumez committed
1139
      self.deleteOldFiles(svn_path, path)
1140
      # add new files and copy
Christophe Dumez's avatar
Christophe Dumez committed
1141 1142 1143
      self.addNewFiles(svn_path, path)
      self.goToWorkingCopy(business_template)
    except (pysvn.ClientError, NotFound, AttributeError, \
1144
    Error), error:
1145
      # Clean up
1146
      shutil.rmtree(path)
1147
      raise error
1148
    # Clean up
Christophe Dumez's avatar
Christophe Dumez committed
1149
    self.activate().removeAllInList([path, ])
1150
    
Christophe Dumez's avatar
Christophe Dumez committed
1151 1152 1153 1154 1155 1156 1157
  def importBT(self, business_template):
    """
     Import business template from local
     working copy
    """
    return business_template.download(self._getWorkingPath(self\
    .getSubversionPath(business_template)))
1158
    
1159 1160
  # Get a list of files and keep only parents
  # Necessary before recursively commit removals
Christophe Dumez's avatar
Christophe Dumez committed
1161 1162 1163 1164 1165 1166
  def cleanChildrenInList(self, path_list):
    """
     Get a list of files and keep only parents
     Necessary before recursively commit removals
    """
    res = path_list
1167 1168 1169
    for path in path_list:
      path = path + '/'
      res = [x for x in res if not x.startswith(path)]
1170
    return res
1171

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

1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221
  # 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
1222 1223 1224
  def deleteOldFiles(self, old_dir, new_dir):
    """ svn del files that have been removed in new dir
    """
1225
    # detect removed files
1226
    files_set = self.getNewFiles(new_dir, old_dir)
1227 1228
    # detect removed directories
    dirs_set = self.getNewDirs(new_dir, old_dir)
1229
    # svn del
Christophe Dumez's avatar
Christophe Dumez committed
1230 1231 1232 1233 1234 1235
    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])
1236
  
Christophe Dumez's avatar
Christophe Dumez committed
1237 1238 1239
  def addNewFiles(self, old_dir, new_dir):
    """ copy files and add new files
    """
1240
    # detect created files
1241
    files_set = self.getNewFiles(old_dir, new_dir)
1242 1243
    # detect created directories
    dirs_set = self.getNewDirs(old_dir, new_dir)
1244
    # Copy files
1245
    copytree(new_dir, old_dir)
1246
    # svn add
Christophe Dumez's avatar
Christophe Dumez committed
1247 1248 1249 1250 1251 1252
    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])
1253
  
Christophe Dumez's avatar
Christophe Dumez committed
1254 1255 1256
  def treeToXML(self, item, business_template) :
    """ Convert tree in memory to XML
    """
1257
    output = '<?xml version="1.0" encoding="UTF-8"?>'+ os.linesep
1258
    output += "<tree id='0'>" + os.linesep
Christophe Dumez's avatar
Christophe Dumez committed
1259
    output = self._treeToXML(item, output, business_template.getTitle(), True)
1260
    output += '</tree>' + os.linesep
1261
    return output
1262
  
1263
  def _treeToXML(self, item, output, relative_path, first) :
Christophe Dumez's avatar
Christophe Dumez committed
1264 1265 1266 1267
    """
     Private function to convert recursively tree 
     in memory to XML
    """
1268
    # Choosing a color coresponding to the status
1269 1270 1271 1272 1273 1274 1275 1276 1277
    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'
1278 1279
    elif status == 'outdated' :
      color = 'purple'
Christophe Dumez's avatar
Christophe Dumez committed
1280
    else :
1281
      color = 'black'
1282
    if isinstance(item, Dir) :
Christophe Dumez's avatar
Christophe Dumez committed
1283
      if first :
1284
        output += '<item open="1" text="%s" id="%s" aCol="%s" '\
Christophe Dumez's avatar
Christophe Dumez committed
1285
        'im0="folder.png" im1="folder_open.png" '\
1286
        'im2="folder.png">'%(item.name, relative_path, color) + os.linesep
1287
        first = False
Christophe Dumez's avatar
Christophe Dumez committed
1288
      else :
1289
        output += '<item text="%s" id="%s" aCol="%s" im0="folder.png" ' \
1290 1291 1292
        '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
1293 1294
        output = self._treeToXML(item.getObjectFromName(it.name), output, \
        os.path.join(relative_path,it.name),first)
1295
      output += '</item>' + os.linesep
1296
    else :
1297
      output += '<item text="%s" id="%s" aCol="%s" im0="document.png"/>'\
1298
                %(item.name, relative_path, color) + os.linesep
1299
    return output
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1300 1301
    
InitializeClass(SubversionTool)