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

from 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
38
import os, re, commands, time, exceptions
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
48
from Products.ERP5.Document.BusinessTemplate import removeAll
49
from xml.sax.saxutils import escape
Aurel's avatar
Aurel committed
50

51

Aurel's avatar
Aurel committed
52 53 54
try:
  from base64 import b64encode, b64decode
except ImportError:
55
  from base64 import encodestring as b64encode, decodestring as b64decode
56 57 58 59 60 61
  
# To keep compatibility with python 2.3
try:
  set
except NameError:
  from sets import Set as set
62 63 64 65

class Error(exceptions.EnvironmentError):
    pass

66 67 68 69 70 71 72 73 74
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
75 76 77 78 79

class SubversionNotAWorkingCopyError(Exception):
  """The base exception class when business template is unknown.
  """
  pass
80 81
      
def copytree(src, dst, symlinks=False):
82
    """Recursively copy a directory tree using copy().
83 84

    If exception(s) occur, an Error is raised with a list of reasons.
85
    dst dir must exist
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101

    If the optional symlinks flag is true, symbolic links in the
    source tree result in symbolic links in the destination tree; if
    it is false, the contents of the files pointed to by symbolic
    links are copied.
    """
    names = os.listdir(src)
    errors = []
    for name in names:
        srcname = os.path.join(src, name)
        dstname = os.path.join(dst, name)
        try:
            if symlinks and os.path.islink(srcname):
                linkto = os.readlink(srcname)
                os.symlink(linkto, dstname)
            elif os.path.isdir(srcname):
102 103
                if not os.path.exists(dstname):
                  os.makedirs(dstname)
104 105
                copytree(srcname, dstname, symlinks)
            else:
106
                copy(srcname, dstname)
107
        except (IOError, os.error), why:
108
            errors.append((srcname, dstname, 'Error: ' + str(why.strerror)))
109 110
    if errors:
        raise Error, errors
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
    
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:
    color='blue'
  return "<font color='%s'>%s</font>"%(color,text,)
    
def colorize(text):
  """Return HTML Code with syntax hightlighting
  """
  # Escape xml before adding html tags
  html = escape(text)
  html = html.replace(' ', '&nbsp;&nbsp;')
  html = html.replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
  # Colorize comments
  p = re.compile(r'#.*')
  html = p.sub(colorizeTag, html)
  # Colorize tags
  p = re.compile(r'&lt;.*?&gt;')
  html = p.sub(colorizeTag, html)
  # Colorize strings
  p = re.compile(r'\".*?\"')
  html = p.sub(colorizeTag, html)
  html = html.replace(os.linesep, os.linesep+"<br>")
  return html
153
  
154 155
class File :
  # Constructor
156 157 158
  def __init__(self, full_path, msg_status) :
    self.full_path = full_path
    self.msg_status = msg_status
159
    self.name = os.path.basename(full_path)
160 161 162 163
## End of File Class

class Dir :
  # Constructor
164 165 166
  def __init__(self, full_path, msg_status) :
    self.full_path = full_path
    self.msg_status = msg_status
167
    self.name = os.path.basename(full_path)
168
    self.sub_dirs = [] # list of sub directories
169 170 171

  # return a list of sub directories' names
  def getSubDirs(self) :
172
    return [d.name for d in self.sub_dirs]
173 174

  # return directory in subdirs given its name
175
  def getDir(self, name):
176
    for d in self.sub_dirs:
177
      if d.name == name:
178 179
        return d
## End of Dir Class
180 181 182 183 184 185 186 187

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

188
  def __init__(self, raw_diff):
189 190 191 192 193
    if '@@' not in raw_diff:
      self.binary=True
      return
    else:
      self.binary=False
194
    self.header = raw_diff.split('@@')[0][:-1]
195
    # Getting file path in header
196
    self.path = self.header.split('====')[0][:-1].strip()
197
    # Getting revisions in header
198
    for line in self.header.split(os.linesep):
199
      if line.startswith('--- '):
200
        tmp = re.search('\\([^)]+\\)$', line)
201
        self.old_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip()
202
      if line.startswith('+++ '):
203
        tmp = re.search('\\([^)]+\\)$', line)
204
        self.new_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip()
205
    # Splitting the body from the header
206
    self.body = os.linesep.join(raw_diff.strip().split(os.linesep)[4:])
207
    # Now splitting modifications
208
    self.children = []
209 210
    first = True
    tmp = []
211
    for line in self.body.split(os.linesep):
212 213
      if line:
        if line.startswith('@@') and not first:
214
          self.children.append(CodeBlock(os.linesep.join(tmp)))
215 216 217 218
          tmp = [line,]
        else:
          first = False
          tmp.append(line)
219
    self.children.append(CodeBlock(os.linesep.join(tmp)))
220
    
221
  def toHTML(self):
222
    # Adding header of the table
223
    if self.binary:
Christophe Dumez's avatar
Christophe Dumez committed
224
      return '<b>Folder or binary file or just no changes!</b><br><br><br>'
225
    
Christophe Dumez's avatar
Christophe Dumez committed
226
    html = '''
227 228 229 230
    <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
231
      <td style="background-color: black;" width="2"></td>
232
      <td style="background-color: grey"><b><center>%s</center></b></td>
Christophe Dumez's avatar
Christophe Dumez committed
233
    </tr>'''%(self.old_revision, self.new_revision)
Christophe Dumez's avatar
Christophe Dumez committed
234
    header_color = 'grey'
235
    for child in self.children:
236
      # Adding line number of the modification
Christophe Dumez's avatar
Christophe Dumez committed
237 238 239 240 241 242
      html += '''<tr height="18px"><td style="background-color: %s">&nbsp;</td><td style="background-color: black;" width="2"></td><td style="background-color: %s">&nbsp;</td></tr>    <tr height="18px">
      <td style="background-color: rgb(68, 132, 255);"><b>Line %s</b></td>
      <td style="background-color: black;" width="2"></td>
      <td style="background-color: rgb(68, 132, 255);"><b>Line %s</b></td>
      </tr>'''%(header_color, header_color, child.old_line, child.new_line)
      header_color = 'white'
243 244 245 246 247 248
      # Adding diff of the modification
      old_code_list = child.getOldCodeList()
      new_code_list = child.getNewCodeList()
      i=0
      for old_line_tuple in old_code_list:
        new_line_tuple = new_code_list[i]
Christophe Dumez's avatar
Christophe Dumez committed
249 250
        new_line = new_line_tuple[0] or ' '
        old_line = old_line_tuple[0] or ' '
251 252
        i+=1
        html += '''    <tr height="18px">
Christophe Dumez's avatar
Christophe Dumez committed
253 254 255
        <td style="background-color: %s">%s</td>
        <td style="background-color: black;" width="2"></td>
        <td style="background-color: %s">%s</td>
256
        </tr>'''%(old_line_tuple[1], escape(old_line).replace(' ', '&nbsp;').replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'), new_line_tuple[1], escape(new_line).replace(' ', '&nbsp;').replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'))
257
    html += '''  </tbody>
Christophe Dumez's avatar
Christophe Dumez committed
258
</table><br><br>'''
259 260 261 262 263 264 265 266 267 268 269 270 271 272
    return html
      

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

273
  def __init__(self, raw_diff):
274
    # Splitting body and header
275 276
    self.body = os.linesep.join(raw_diff.split(os.linesep)[1:])
    self.header = raw_diff.split(os.linesep)[0]
277
    # Getting modifications lines
278 279
    tmp = re.search('^@@ -\d+', self.header)
    self.old_line = tmp.string[tmp.start():tmp.end()][4:]
Christophe Dumez's avatar
Christophe Dumez committed
280 281
    tmp = re.search('\+\d+', self.header)
    self.new_line = tmp.string[tmp.start():tmp.end()][1:]
282 283
    # Splitting modifications in SubCodeBlocks
    in_modif = False
284
    self.children = []
285
    tmp=[]
286
    for line in self.body.split(os.linesep):
287 288 289 290 291
      if line:
        if (line.startswith('+') or line.startswith('-')):
          if in_modif:
            tmp.append(line)
          else:
292
            self.children.append(SubCodeBlock(os.linesep.join(tmp)))
293 294 295 296
            tmp = [line,]
            in_modif = True
        else:
            if in_modif:
297
              self.children.append(SubCodeBlock(os.linesep.join(tmp)))
298 299 300 301
              tmp = [line,]
              in_modif = False
            else:
              tmp.append(line)
302
    self.children.append(SubCodeBlock(os.linesep.join(tmp)))
303 304
    
  # Return code before modification
305
  def getOldCodeList(self):
306
    tmp = []
307
    for child in self.children:
308 309 310 311
      tmp.extend(child.getOldCodeList())
    return tmp
    
  # Return code after modification
312
  def getNewCodeList(self):
313
    tmp = []
314
    for child in self.children:
315 316 317 318 319
      tmp.extend(child.getNewCodeList())
    return tmp
    
# a SubCodeBlock contain 0 or 1 modification (not more)
class SubCodeBlock:
320
  def __init__(self, code):
321 322
    self.body = code
    self.modification = self._getModif()
Christophe Dumez's avatar
Christophe Dumez committed
323 324
    self.old_code_length = self._getOldCodeLength()
    self.new_code_length = self._getNewCodeLength()
325
    # Choosing background color
326 327 328 329 330 331
    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
332
    else: # addition
333
      self.color = 'rgb(83, 253, 74);'#light green
334
    
335
  def _getModif(self):
336 337
    nb_plus = 0
    nb_minus = 0
338
    for line in self.body.split(os.linesep):
339 340 341 342 343 344
      if line.startswith("-"):
        nb_minus-=1
      elif line.startswith("+"):
        nb_plus+=1
    if (nb_plus==0 and nb_minus==0):
      return 'none'
Christophe Dumez's avatar
Christophe Dumez committed
345 346 347 348
    if (nb_minus==0):
      return 'addition'
    if (nb_plus==0):
      return 'deletion'
349
    return 'change'
Christophe Dumez's avatar
Christophe Dumez committed
350 351 352
      
  def _getOldCodeLength(self):
    nb_lines = 0
353
    for line in self.body.split(os.linesep):
Christophe Dumez's avatar
Christophe Dumez committed
354 355 356 357 358 359
      if not line.startswith("+"):
        nb_lines+=1
    return nb_lines
      
  def _getNewCodeLength(self):
    nb_lines = 0
360
    for line in self.body.split(os.linesep):
Christophe Dumez's avatar
Christophe Dumez committed
361 362 363
      if not line.startswith("-"):
        nb_lines+=1
    return nb_lines
364
  
365
  # Return code before modification
366 367
  def getOldCodeList(self):
    if self.modification=='none':
368
      old_code = [(x, 'white') for x in self.body.split(os.linesep)]
Christophe Dumez's avatar
Christophe Dumez committed
369
    elif self.modification=='change':
370
      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
371 372 373 374
      # we want old_code_list and new_code_list to have the same length
      if(self.old_code_length < self.new_code_length):
        filling = [(None, self.color)]*(self.new_code_length-self.old_code_length)
        old_code.extend(filling)
375
    else: # deletion or addition
376
      old_code = [self._getOldCodeList(x) for x in self.body.split(os.linesep)]
377
    return old_code
378
  
379
  def _getOldCodeList(self, line):
380
    if line.startswith('+'):
381
      return (None, self.color)
382
    if line.startswith('-'):
383 384
      return (' '+line[1:], self.color)
    return (line, self.color)
385 386
  
  # Return code after modification
387 388
  def getNewCodeList(self):
    if self.modification=='none':
389
      new_code = [(x, 'white') for x in self.body.split(os.linesep)]
Christophe Dumez's avatar
Christophe Dumez committed
390
    elif self.modification=='change':
391
      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
392 393 394 395
      # we want old_code_list and new_code_list to have the same length
      if(self.new_code_length < self.old_code_length):
        filling = [(None, self.color)]*(self.old_code_length-self.new_code_length)
        new_code.extend(filling)
396
    else: # deletion or addition
397
      new_code = [self._getNewCodeList(x) for x in self.body.split(os.linesep)]
398
    return new_code
399
  
400
  def _getNewCodeList(self, line):
401
    if line.startswith('-'):
402
      return (None, self.color)
403
    if line.startswith('+'):
404 405
      return (' '+line[1:], self.color)
    return (line, self.color)
406
  
407
class SubversionTool(BaseTool, UniqueObject, Folder):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
408 409 410 411 412 413 414 415 416
  """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'
417 418 419
  
  top_working_path = getConfiguration().instancehome
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
  # Declarative Security
  security = ClassSecurityInfo()

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

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

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

  # Filter content (ZMI))
  def filtered_meta_types(self, user=None):
      # Filters the list of available meta types.
      all = SubversionTool.inheritedAttribute('filtered_meta_types')(self)
      meta_types = []
      for meta_type in self.all_meta_types():
          if meta_type['name'] in self.allowed_types:
              meta_types.append(meta_type)
      return meta_types
    
Christophe Dumez's avatar
Christophe Dumez committed
451
  # path is the path in svn working copy
452 453
  # return edit_path in zodb to edit it
  # return '#' if no zodb path is found
Christophe Dumez's avatar
Christophe Dumez committed
454 455
  def editPath(self, bt, path):
    """Return path to edit file
456
       path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
457
    """
458
    path = self.relativeToAbsolute(path, bt).replace('\\', '/')
459
    if 'bt' in path.split('/'):
460
      # not in zodb
Christophe Dumez's avatar
Christophe Dumez committed
461
      return '#'
462 463 464
    # if file have been deleted then not in zodb
    if not os.path.exists(path):
      return '#'
465
    svn_path = self.getSubversionPath(bt).replace('\\', '/')
Christophe Dumez's avatar
Christophe Dumez committed
466 467
    edit_path = path.replace(svn_path, '').strip()
    if edit_path == '':
468 469
      # not in zodb 
      return '#'
470
    if edit_path[0] == '/':
471
      edit_path = edit_path[1:]
Christophe Dumez's avatar
Christophe Dumez committed
472 473
    edit_path = '/'.join(edit_path.split('/')[1:]).strip()
    if edit_path == '':
474 475
      # not in zodb 
      return '#'
476
    # remove file extension
Christophe Dumez's avatar
Christophe Dumez committed
477 478
    edit_path = os.path.splitext(edit_path)[0]
    # Add beginning and end of url
479
    edit_path = os.path.join(bt.REQUEST["BASE2"], edit_path, 'manage_main')
Christophe Dumez's avatar
Christophe Dumez committed
480 481
    return edit_path
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
482 483 484 485 486 487 488
  def _encodeLogin(self, realm, user, password):
    # Encode login information.
    return b64encode(dumps((realm, user, password)))

  def _decodeLogin(self, login):
    # Decode login information.
    return loads(b64decode(login))
489 490 491 492
  
  def goToWorkingCopy(self, bt):
      working_path = self.getSubversionPath(bt)
      os.chdir(working_path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
493
    
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508
  def setLogin(self, realm, user, password):
    """Set login information.
    """
    # Get existing login information. Filter out old information.
    login_list = []
    request = self.REQUEST
    cookie = request.get(self.login_cookie_name)
    if cookie:
      for login in cookie.split(','):
        if self._decodeLogin(login)[0] != realm:
          login_list.append(login)
    # Set the cookie.
    response = request.RESPONSE
    login_list.append(self._encodeLogin(realm, user, password))
    value = ','.join(login_list)
Christophe Dumez's avatar
Christophe Dumez committed
509
    expires = (DateTime() + 7).toZone('GMT').rfc822()
510
    request.set(self.login_cookie_name, value)
511
    response.setCookie(self.login_cookie_name, value, path = '/', expires = expires)
512

Yoshinori Okuji's avatar
Yoshinori Okuji committed
513 514 515 516 517 518 519 520 521
  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
522 523 524 525 526 527 528 529
      
  def getHeader(self, bt, file):
    file = self.relativeToAbsolute(file, bt)
    header = "<b><a href='BusinessTemplate_viewSvnShowFile?file="+file+"'>" + file + "</a></b>"
    edit_path = self.editPath(bt, file)
    if edit_path != '#':
      header += "&nbsp;&nbsp;<a href='"+self.editPath(bt, file)+"'><img src='imgs/edit.png' border='0'></a>"
    return header
Yoshinori Okuji's avatar
Yoshinori Okuji committed
530 531 532 533 534 535 536 537 538 539

  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
540
    trust_item_list, permanent = loads(b64decode(trust))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
541
    return dict(trust_item_list), permanent
542
  
543 544
  def diffHTML(self, file_path, bt, revision1=None, revision2=None):
    raw_diff = self.diff(file_path, bt, revision1, revision2)
545
    return DiffFile(raw_diff).toHTML()
Christophe Dumez's avatar
Christophe Dumez committed
546
  
Christophe Dumez's avatar
Christophe Dumez committed
547
  # Display a file content in HTML with syntax highlighting
Christophe Dumez's avatar
Christophe Dumez committed
548
  def fileHTML(self, bt, file_path):
549
    file_path = self.relativeToAbsolute(file_path, bt)
550
    file = open(file_path, 'r')
551 552 553
    if os.path.exists(file_path):
      if os.path.isdir(file_path):
        text = "<b>"+file_path+"</b><hr>"
554
        text += file_path +" is a folder!"
555 556
      else:
        head = "<b>"+file_path+"</b>  <a href='"+self.editPath(bt, file_path)+"'><img src='imgs/edit.png' border='0'></a><hr>"
557
        text = head + colorize(file.read())
558 559
    else:
      # see if tmp file is here (svn deleted file)
560
      if file_path[-1]==os.sep:
561
        file_path=file_path[:-1]
562 563
      filename = file_path.split(os.sep)[-1]
      tmp_path = os.sep.join(file_path.split(os.sep)[:-1])
564
      tmp_path = os.path.join(tmp_path,'.svn','text-base',filename+'.svn-base')
565 566
      if os.path.exists(tmp_path):
        head = "<b>"+tmp_path+"</b> (svn temporary file)<hr>"
567
        text = head + colorize(file.read())
568 569
      else : # does not exist
        text = "<b>"+file_path+"</b><hr>"
570
        text += file_path +" does not exist!"
571 572
    file.close()
    return text
573
      
Yoshinori Okuji's avatar
Yoshinori Okuji committed
574 575 576 577 578 579 580 581 582
  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
583
      trust_list.append(cookie)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
584 585 586 587
    # Set the cookie.
    response = request.RESPONSE
    trust_list.append(self._encodeSSLTrust(trust_dict, permanent))
    value = ','.join(trust_list)
Christophe Dumez's avatar
Christophe Dumez committed
588
    expires = (DateTime() + 7).toZone('GMT').rfc822()
589
    request.set(self.ssl_trust_cookie_name, value)
590
    response.setCookie(self.ssl_trust_cookie_name, value, path = '/', expires = expires)
Christophe Dumez's avatar
Christophe Dumez committed
591 592 593
    
  def acceptSSLPerm(self, trust_dict):
    self.acceptSSLServer(self, trust_dict, True)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610

  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)
611 612 613 614 615 616 617 618 619
  
  security.declareProtected('Import/Export objects', 'getSubversionPath')
  # with_name : with business template name at the end of the path
  def getSubversionPath(self, bt, 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)
    wc_list = self.getPortalObject().portal_preferences.getPreferredSubversionWorkingCopyList()
620 621 622
    if not wc_list:
      wc_list = self.getPortalObject().portal_preferences.default_site_preference.getPreferredSubversionWorkingCopyList()
      if not wc_list:
623
        raise SubversionPreferencesError, 'Please set at least one Subversion Working Copy in preferences first.'
624
    if len(wc_list) == 0 :
625
      raise SubversionPreferencesError, 'Please set at least one Subversion Working Copy in preferences first.'
626
    bt_name = bt.getTitle()
627
    for wc in wc_list:
628
      wc = self._getWorkingPath(wc)
629 630
      if not os.path.exists(os.path.join(wc, '.svn')):
        raise SubversionNotAWorkingCopyError, "You must check out working copies in this directory: "+wc+" or choose another path in portal preferences."
631 632 633 634 635 636 637
      if bt_name in os.listdir(wc) :
        wc_path = os.path.join(wc, bt_name)
        if os.path.isdir(wc_path):
          if with_name:
            return wc_path
          else:
            return os.sep.join(wc_path.split(os.sep)[:-1])
638
    raise SubversionUnknownBusinessTemplateError, "Could not find '"+bt_name+"' at first level of working copies."
639
    
640 641 642 643 644 645 646 647 648 649 650
  def getTopWorkingPath(self):
    return self.top_working_path

  def _getWorkingPath(self, path):
    #if path[0] != '/':
    #  path = os.path.join(self.top_working_path, path)
    #path = os.path.abspath(path)
    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
651
  security.declareProtected('Import/Export objects', 'update')
652
  def update(self, bt):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
653 654
    """Update a working copy.
    """
655
    path = self._getWorkingPath(self.getSubversionPath(bt))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
656
    client = self._getClient()
657
    # Revert local changes in working copy first to import a "pure" BT after update
658 659 660 661
    self.revert(path=path, recurse=True)
    # Update from SVN
    client.update(path)
    # Import in zodb
Christophe Dumez's avatar
Christophe Dumez committed
662
    return self.importBT(bt)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
663

664 665 666 667
  security.declareProtected('Import/Export objects', 'switch')
  def switch(self, bt, url):
    """switch SVN repository for a working copy.
    """
668
    path = self._getWorkingPath(self.getSubversionPath(bt))
669
    client = self._getClient()
670 671
    if url[-1] == '/' :
      url = url[:-1]
672
    # Update from SVN
673
    client.switch(path=path, url=url)
674
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
675
  security.declareProtected('Import/Export objects', 'add')
676 677
  # path can be a list or not (relative or absolute)
  def add(self, path, bt=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
678 679
    """Add a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
680
    if bt is not None:
681
      if isinstance(path, list) :
682
        path = [self._getWorkingPath(self.relativeToAbsolute(x, bt)) for x in path]
683
      else:
684
        path = self._getWorkingPath(self.relativeToAbsolute(path, bt))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
685
    client = self._getClient()
686
    return client.add(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
687

688
  security.declareProtected('Import/Export objects', 'info')
689
  def info(self, bt):
690 691
    """return info of working copy
    """
692
    working_copy = self._getWorkingPath(self.getSubversionPath(bt))
693 694 695
    client = self._getClient()
    return client.info(working_copy)
  
Christophe Dumez's avatar
Christophe Dumez committed
696
  security.declareProtected('Import/Export objects', 'log')
697 698
  # path can be absolute or relative
  def log(self, path, bt):
Christophe Dumez's avatar
Christophe Dumez committed
699 700 701
    """return log of a file or dir
    """
    client = self._getClient()
702
    return client.log(self._getWorkingPath(self.relativeToAbsolute(path, bt)))
Christophe Dumez's avatar
Christophe Dumez committed
703
  
704
  security.declareProtected('Import/Export objects', 'cleanup')
705
  def cleanup(self, bt):
706 707
    """remove svn locks in working copy
    """
708
    working_copy = self._getWorkingPath(self.getSubversionPath(bt))
709 710 711
    client = self._getClient()
    return client.cleanup(working_copy)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
712
  security.declareProtected('Import/Export objects', 'remove')
713 714
  # path can be a list or not (relative or absolute)
  def remove(self, path, bt=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
715 716
    """Remove a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
717
    if bt is not None:
718
      if isinstance(path, list) :
719
        path = [self._getWorkingPath(self.relativeToAbsolute(x, bt)) for x in path]
720
      else:
721
        path = self._getWorkingPath(self.relativeToAbsolute(path, bt))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
722
    client = self._getClient()
723
    return client.remove(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
724 725 726 727 728 729

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

Christophe Dumez's avatar
Christophe Dumez committed
732
  security.declareProtected('Import/Export objects', 'ls')
733 734
  # path can be relative or absolute
  def ls(self, path, bt):
Christophe Dumez's avatar
Christophe Dumez committed
735 736 737
    """Display infos about a file.
    """
    client = self._getClient()
738
    return client.ls(self._getWorkingPath(self.relativeToAbsolute(path, bt)))
Christophe Dumez's avatar
Christophe Dumez committed
739

Yoshinori Okuji's avatar
Yoshinori Okuji committed
740
  security.declareProtected('Import/Export objects', 'diff')
741 742
  # path can be relative or absolute
  def diff(self, path, bt, revision1=None, revision2=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
743 744 745
    """Make a diff for a file or a directory.
    """
    client = self._getClient()
746
    return client.diff(self._getWorkingPath(self.relativeToAbsolute(path, bt)), revision1, revision2)
747
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
748
  security.declareProtected('Import/Export objects', 'revert')
749
  # path can be absolute or relative
750
  def revert(self, path, bt=None, recurse=False):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
751 752 753
    """Revert local changes in a file or a directory.
    """
    client = self._getClient()
754
    if not isinstance(path, list) :
755
      path = [self._getWorkingPath(self.relativeToAbsolute(path))]
Christophe Dumez's avatar
Christophe Dumez committed
756
    if bt is not None:
757
      path = [self._getWorkingPath(self.relativeToAbsolute(x, bt)) for x in path]
Christophe Dumez's avatar
Christophe Dumez committed
758
    client.revert(path, recurse)
759 760 761 762 763 764 765 766 767

  security.declareProtected('Import/Export objects', 'revertZODB')
  # path can be absolute or relative
  def revertZODB(self, bt, added_files=None, other_files=None, recurse=False):
    """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
768
    # Transform params to list if they are not already lists
769 770 771 772 773 774 775 776 777 778 779
    if not added_files :
      added_files = []
    if not other_files :
      other_files = []
    if not isinstance(added_files, list) :
      added_files=[added_files]
    if not isinstance(other_files, list) :
      other_files=[other_files]
    
    # Reinstall removed or modified files
    for p in other_files :
780
      path_list = self._getWorkingPath(p).split(os.sep)
781 782 783 784
      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
785
          tmp = os.path.splitext(tmp)[0]
786
          object_to_update[tmp] = 'install'
787
    path_added_list = []
788 789
    # remove added files
    for p in added_files :
790
      path_list = self._getWorkingPath(p).split(os.sep)
791 792 793 794
      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
795 796
          tmp = os.path.splitext(tmp)[0]
          path_added_list.append(tmp)
797 798 799 800 801 802 803 804 805 806 807 808 809
    ## hack to remove objects
    # Create a temporary bt with objects to delete
    tmp_bt = getToolByName(bt, 'portal_templates').newContent(portal_type="Business Template")
    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
    bt.portal_templates.manage_delObjects(ids=tmp_bt.getId())
810 811 812 813 814 815
    #revert changes
    added_files.extend(other_files)
    to_revert = [self.relativeToAbsolute(x, bt) for x in added_files]
    if len(to_revert) != 0 :
      client.revert(to_revert, recurse)
      # Partially reinstall installed bt
816
      installed_bt = bt.portal_templates.getInstalledBusinessTemplate(                                                          bt.getTitle())
817
      installed_bt.reinstall(object_to_update=object_to_update, force=0)
Christophe Dumez's avatar
Christophe Dumez committed
818 819 820 821 822 823 824 825
    
  security.declareProtected('Import/Export objects', 'resolved')
  # path can be absolute or relative
  def resolved(self, path, bt):
    """remove conflicted status
    """
    client = self._getClient()
    if isinstance(path, list) :
826
      path = [self._getWorkingPath(self.relativeToAbsolute(x, bt)) for x in path]
Christophe Dumez's avatar
Christophe Dumez committed
827
    else:
828
      path = self._getWorkingPath(self.relativeToAbsolute(path, bt))
Christophe Dumez's avatar
Christophe Dumez committed
829
    return client.resolved(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
830

831 832 833 834 835 836 837 838 839 840
  def relativeToAbsolute(self, path, bt) :
    if path[0] == os.sep:
      # already absolute
      return path
    # relative path
    if path.split(os.sep)[0] == bt.getTitle():
      return os.path.join(self.getSubversionPath(bt, False), path)
    else:
      return os.path.join(self.getSubversionPath(bt), path)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
841
  security.declareProtected('Import/Export objects', 'checkin')
842 843
  # path can be relative or absolute (can be a list of paths too)
  def checkin(self, path, bt, log_message=None, recurse=True):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
844 845
    """Commit local changes.
    """
846
    if isinstance(path, list) :
847
      path = [self._getWorkingPath(self.relativeToAbsolute(x, bt)) for x in path]
848
    else:
849
      path = self._getWorkingPath(self.relativeToAbsolute(path, bt))
850
    client = self._getClient()
851
    return client.checkin(path, log_message, recurse)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
852 853 854 855 856 857

  security.declareProtected('Import/Export objects', 'status')
  def status(self, path, **kw):
    """Get status.
    """
    client = self._getClient()
858
    return client.status(self._getWorkingPath(path), **kw)
859
  
860 861 862 863 864
  security.declareProtected('Import/Export objects', 'unversionedFiles')
  def unversionedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
865
    status_list = client.status(self._getWorkingPath(path), **kw)
866 867 868 869 870 871 872 873
    unversioned_list = []
    for statusObj in status_list:
      if str(statusObj.getTextStatus()) == "unversioned":
        my_dict = {}
        my_dict['uid'] = statusObj.getPath()
        unversioned_list.append(my_dict)
    return unversioned_list
      
874 875 876 877 878
  security.declareProtected('Import/Export objects', 'conflictedFiles')
  def conflictedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
879
    status_list = client.status(self._getWorkingPath(path), **kw)
880 881 882 883 884 885 886 887
    conflicted_list = []
    for statusObj in status_list:
      if str(statusObj.getTextStatus()) == "conflicted":
        my_dict = {}
        my_dict['uid'] = statusObj.getPath()
        conflicted_list.append(my_dict)
    return conflicted_list

888 889 890 891 892
  security.declareProtected('Import/Export objects', 'removeAllInList')
  def removeAllInList(self, list):
    """Remove all files and folders in list
    """
    for file in list:
893
      removeAll(file)
894
    
895
  def getModifiedTree(self, bt, show_unmodified=False) :
Christophe Dumez's avatar
Christophe Dumez committed
896
    # Remove trailing slash if it's present
897
    path = self._getWorkingPath(self.getSubversionPath(bt))
Christophe Dumez's avatar
Christophe Dumez committed
898
    root = Dir(path, "normal")
899
    somethingModified = False
900
    
901
    for statusObj in self.status(path) :
902
      # can be (normal, added, modified, deleted, conflicted, unversioned)
903
      msg_status = statusObj.getTextStatus()
904
      if (show_unmodified or str(msg_status) != "normal") and str(msg_status) != "unversioned":
905
        somethingModified = True
Christophe Dumez's avatar
Christophe Dumez committed
906
        full_path = statusObj.getPath()
907
        full_path_list = full_path.split(os.sep)[1:]
Christophe Dumez's avatar
Christophe Dumez committed
908
        relative_path_list = full_path[len(path)+1:].split(os.sep)
909
        # Processing entry
Christophe Dumez's avatar
Christophe Dumez committed
910 911 912
        filename = relative_path_list[-1]
        # Needed or files will be both File & Dir objects
        relative_path_list = relative_path_list[:-1]
913
        parent = root
Christophe Dumez's avatar
Christophe Dumez committed
914
        i = len(path.split(os.sep))
Christophe Dumez's avatar
Christophe Dumez committed
915 916 917
        
        for d in relative_path_list :
          if d :
918
            full_pathOfd = os.sep+os.sep.join(full_path_list[:i]).strip()
919
            if d not in parent.getSubDirs() :
920
              parent.sub_dirs.append(Dir(full_pathOfd, "normal"))
921
            parent = parent.getDir(d)
Christophe Dumez's avatar
Christophe Dumez committed
922 923
          i += 1
            
Christophe Dumez's avatar
Christophe Dumez committed
924
        if os.path.isdir(full_path) :
925 926
          if full_path == parent.full_path :
            parent.msg_status = str(msg_status)
927 928
          elif filename not in parent.getSubDirs() :
            parent.sub_dirs.append(Dir(filename, str(msg_status)))
Christophe Dumez's avatar
Christophe Dumez committed
929
          else :
930
            tmp = parent.getDir(filename)
931
            tmp.msg_status = str(msg_status)
Christophe Dumez's avatar
Christophe Dumez committed
932
        else :
Christophe Dumez's avatar
Christophe Dumez committed
933
          parent.sub_dirs.append(File(full_path, str(msg_status)))
934
    return somethingModified and root
935
  
936
  def extractBT(self, bt):
937
    bt.build()
938
    svn_path = self._getWorkingPath(self.getSubversionPath(bt) + os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
939
    path = mktemp() + os.sep
940 941
    bt.export(path=path, local=1)
    # svn del deleted files
942
    self.deleteOldFiles(svn_path, path, bt)
943
    # add new files and copy
944
    self.addNewFiles(svn_path, path, bt)
945
    self.goToWorkingCopy(bt)
946
    # Clean up
947
    self.activate().removeAllInList([path,])
948 949
    
  def importBT(self, bt):
950
    return bt.download(self._getWorkingPath(self.getSubversionPath(bt)))
951 952 953 954 955 956 957 958
  
  # Get a list of files and keep only parents
  # Necessary before recursively commit removals
  def cleanChildrenInList(self, list):
    res = list
    for file in list:
      res = [x for x in res if file == x or file not in x]
    return res
959

960 961
  # return a set with directories present in the directory
  def getSetDirsForDir(self, directory):
962 963 964 965 966 967 968
    dir_set = set()
    for root, dirs, files in os.walk(directory):
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
      # get Directories
      for name in dirs:
969
        i = root.replace(directory, '').count(os.sep)
970
        f = os.path.join(root, name)
971
        dir_set.add((i, f.replace(directory,'')))
972 973 974 975 976 977 978 979 980
    return dir_set
      
  # return a set with files present in the directory
  def getSetFilesForDir(self, directory):
    dir_set = set()
    for root, dirs, files in os.walk(directory):
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
981
      # get Files
982 983
      for name in files:
        i = root.replace(directory, '').count(os.sep)
984
        f = os.path.join(root, name)
985
        dir_set.add((i, f.replace(directory,'')))
986
    return dir_set
987
  
988
  # return files present in new_dir but not in old_dir
989 990
  # return a set of relative paths
  def getNewFiles(self, old_dir, new_dir):
991 992 993 994
    if old_dir[-1] != os.sep:
      old_dir += os.sep
    if new_dir[-1] != os.sep:
      new_dir += os.sep
995 996
    old_set = self.getSetFilesForDir(old_dir)
    new_set = self.getSetFilesForDir(new_dir)
997 998
    return new_set.difference(old_set)

999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009
  # 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)
    
1010
  # svn del files that have been removed in new dir
1011
  def deleteOldFiles(self, old_dir, new_dir, bt):
1012
    # detect removed files
1013
    files_set = self.getNewFiles(new_dir, old_dir)
1014 1015
    # detect removed directories
    dirs_set = self.getNewDirs(new_dir, old_dir)
1016
    # svn del
1017 1018 1019 1020 1021 1022
    list = [x for x in files_set]
    list.sort()
    self.remove([os.path.join(old_dir, x[1]) for x in list])
    list = [x for x in dirs_set]
    list.sort()
    self.remove([os.path.join(old_dir, x[1]) for x in list])
1023
  
1024 1025
  # copy files and add new files
  def addNewFiles(self, old_dir, new_dir, bt):
1026
    # detect created files
1027
    files_set = self.getNewFiles(old_dir, new_dir)
1028 1029
    # detect created directories
    dirs_set = self.getNewDirs(old_dir, new_dir)
1030
    # Copy files
1031
    copytree(new_dir, old_dir)
1032
    # svn add
1033 1034 1035 1036 1037 1038
    list = [x for x in dirs_set]
    list.sort()
    self.add([os.path.join(old_dir, x[1]) for x in list])
    list = [x for x in files_set]
    list.sort()
    self.add([os.path.join(old_dir, x[1]) for x in list])
1039
  
1040
  def treeToXML(self, item, bt) :
1041
    working_copy = self._getWorkingPath(self.getSubversionPath(bt, False) + os.sep)
1042 1043
    output = "<?xml version='1.0' encoding='iso-8859-1'?>"+ os.linesep
    output += "<tree id='0'>" + os.linesep
1044
    output = self._treeToXML(item, working_copy, output, 1, True)
1045 1046
    output += "</tree>" + os.linesep
    return output
1047
  
1048
  def _treeToXML(self, item, working_copy, output, ident, first) :
1049
    # Choosing a color coresponding to the status
1050
    itemStatus = item.msg_status
Christophe Dumez's avatar
Christophe Dumez committed
1051 1052
    if itemStatus == 'added' :
      itemColor='green'
1053
    elif itemStatus == 'modified' or  itemStatus == 'replaced' :
Christophe Dumez's avatar
Christophe Dumez committed
1054 1055 1056
      itemColor='orange'
    elif itemStatus == 'deleted' :
      itemColor='red'
1057 1058
    elif itemStatus == 'conflicted' :
      itemColor='grey'
Christophe Dumez's avatar
Christophe Dumez committed
1059 1060
    else :
      itemColor='black'
1061 1062
    if isinstance(item, Dir) :
      for i in range(ident) :
1063
        output += '\t'
Christophe Dumez's avatar
Christophe Dumez committed
1064
      if first :
1065
        output += '<item open="1" text="%s" id="%s" aCol="%s" '\
Christophe Dumez's avatar
Christophe Dumez committed
1066
        'im0="folder.png" im1="folder_open.png" '\
1067 1068
        'im2="folder.png">'%(item.name, item.full_path.replace(working_copy, ''), itemColor,) + os.linesep
        first = False
Christophe Dumez's avatar
Christophe Dumez committed
1069
      else :
1070
        output += '<item text="%s" id="%s" aCol="%s" im0="folder.png" ' \
1071
      'im1="folder_open.png" im2="folder.png">'%(item.name,
1072
item.full_path.replace(working_copy, ''), itemColor,) + os.linesep
1073
      for it in item.sub_dirs:
1074
        ident += 1
1075
        output = self._treeToXML(item.getDir(it.name), working_copy, output, ident,
Christophe Dumez's avatar
Christophe Dumez committed
1076
first)
1077 1078
        ident -= 1
      for i in range(ident) :
1079 1080
        output += '\t'
      output += '</item>' + os.linesep
1081 1082
    else :
      for i in range(ident) :
1083 1084
        output += '\t'
      output += '<item text="%s" id="%s" aCol="%s" im0="document.png"/>'\
1085
                %(item.name, item.full_path.replace(working_copy, ''), itemColor,) + os.linesep
1086
    return output
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1087 1088
    
InitializeClass(SubversionTool)