SubversionTool.py 37.3 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
from AccessControl import ClassSecurityInfo
33
from Products.ERP5Type.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
from Products.ERP5Type import Permissions
from Products.ERP5Subversion import _dtmldir
37
from Products.ERP5Type.DiffUtils import DiffFile
Yoshinori Okuji's avatar
Yoshinori Okuji committed
38
from Products.ERP5Subversion.SubversionClient import newSubversionClient
39
import os, re
Yoshinori Okuji's avatar
Yoshinori Okuji committed
40 41 42
from DateTime import DateTime
from cPickle import dumps, loads
from App.config import getConfiguration
43
from tempfile import gettempdir, mktemp
44
from Products.CMFCore.utils import getToolByName
45
import shutil
46
from xml.sax.saxutils import escape
47
from dircache import listdir
48
from OFS.Traversable import NotFound
49
from Products.ERP5Type.patches.copyTree import copytree, Error
50
from Products.ERP5Type.patches.cacheWalk import cacheWalk
51
import transaction
Aurel's avatar
Aurel committed
52

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

58
from base64 import b64encode, b64decode
59
from warnings import warn
Christophe Dumez's avatar
Christophe Dumez committed
60 61 62 63

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

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

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

115 116 117 118 119 120 121 122 123
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
124 125

class SubversionNotAWorkingCopyError(Exception):
126
  """The base exception class when directory is not a working copy
127 128
  """
  pass
129

130 131 132 133 134
class SubversionConflictError(Exception):
  """The base exception class when there is a conflict
  """
  pass

135 136
class SubversionSecurityError(Exception): pass

137 138 139 140 141
class SubversionBusinessTemplateNotInstalled(Exception):
  """ Exception called when the business template is not installed
  """
  pass

142 143 144 145 146
class UnauthorizedAccessToPath(Exception):
  """ When path is not in zope home instance
  """
  pass

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

190
class SubversionTool(BaseTool):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
191 192 193 194 195 196 197 198 199
  """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'
200 201 202
  
  top_working_path = getConfiguration().instancehome
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
203 204 205 206 207 208
  # Declarative Security
  security = ClassSecurityInfo()

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

Christophe Dumez's avatar
Christophe Dumez committed
209
  # path is the path in svn working copy
210 211
  # return edit_path in zodb to edit it
  # return '#' if no zodb path is found
Christophe Dumez's avatar
Christophe Dumez committed
212
  def editPath(self, business_template, path):
Christophe Dumez's avatar
Christophe Dumez committed
213
    """Return path to edit file
214
       path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
215
    """
Christophe Dumez's avatar
Christophe Dumez committed
216
    path = self.relativeToAbsolute(path, business_template).replace('\\', '/')
217
    if 'bt' in path.split('/'):
218
      # not in zodb
Christophe Dumez's avatar
Christophe Dumez committed
219
      return '#'
220 221 222
    # if file have been deleted then not in zodb
    if not os.path.exists(path):
      return '#'
Christophe Dumez's avatar
Christophe Dumez committed
223
    svn_path = self.getSubversionPath(business_template).replace('\\', '/')
Christophe Dumez's avatar
Christophe Dumez committed
224 225
    edit_path = path.replace(svn_path, '').strip()
    if edit_path == '':
226 227
      # not in zodb 
      return '#'
228
    if edit_path[0] == '/':
229
      edit_path = edit_path[1:]
Christophe Dumez's avatar
Christophe Dumez committed
230 231
    edit_path = '/'.join(edit_path.split('/')[1:]).strip()
    if edit_path == '':
232 233
      # not in zodb 
      return '#'
234
    # remove file extension
Christophe Dumez's avatar
Christophe Dumez committed
235 236
    edit_path = os.path.splitext(edit_path)[0]
    # Add beginning and end of url
Christophe Dumez's avatar
Christophe Dumez committed
237 238
    edit_path = os.path.join(business_template.REQUEST["BASE2"], \
    edit_path, 'manage_main')
Christophe Dumez's avatar
Christophe Dumez committed
239 240
    return edit_path
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
241
  def _encodeLogin(self, realm, user, password):
Christophe Dumez's avatar
Christophe Dumez committed
242 243
    """ Encode login information.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
244 245 246
    return b64encode(dumps((realm, user, password)))

  def _decodeLogin(self, login):
Christophe Dumez's avatar
Christophe Dumez committed
247 248
    """ Decode login information.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
249
    return loads(b64decode(login))
250
  
Christophe Dumez's avatar
Christophe Dumez committed
251 252 253 254 255
  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
256
    
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
  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)
272
    expires = (DateTime() + 1).toZone('GMT').rfc822()
273
    request.set(self.login_cookie_name, value)
Christophe Dumez's avatar
Christophe Dumez committed
274 275
    response.setCookie(self.login_cookie_name, value, path = '/', \
    expires = expires)
276

Yoshinori Okuji's avatar
Yoshinori Okuji committed
277 278 279 280 281 282 283 284 285
  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
286
      
Christophe Dumez's avatar
Christophe Dumez committed
287
  def getHeader(self, business_template, file_path):
288
    file_path = self._getWorkingPath(self.relativeToAbsolute(file_path, business_template))
289 290
    header = '<a style="font-weight: bold" href="BusinessTemplate_viewSvnShowFile?file=' + \
    file_path + '">' + file_path + '</a>'
Christophe Dumez's avatar
Christophe Dumez committed
291
    edit_path = self.editPath(business_template, file_path)
292
    if edit_path != '#':
293
      header += '&nbsp;&nbsp;<a href="'+self.editPath(business_template, \
294
      file_path) + '"><img src="imgs/edit.png" style="border: 0" alt="edit" /></a>'
295
    return header
Yoshinori Okuji's avatar
Yoshinori Okuji committed
296 297 298 299 300 301 302 303 304 305

  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
306
    trust_item_list, permanent = loads(b64decode(trust))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
307
    return dict(trust_item_list), permanent
308
  
309 310 311
  def getPreferredUsername(self):
    """return username in preferences if set of the current username
    """
Christophe Dumez's avatar
Christophe Dumez committed
312 313
    username = self.getPortalObject().portal_preferences\
    .getPreferredSubversionUserName()
314 315 316 317 318
    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
319 320 321 322 323
  def diffHTML(self, file_path, business_template, revision1=None, \
  revision2=None):
    """ Return HTML diff
    """
    raw_diff = self.diff(file_path, business_template, revision1, revision2)
324
    return DiffFile(raw_diff).toHTML()
Christophe Dumez's avatar
Christophe Dumez committed
325
  
Christophe Dumez's avatar
Christophe Dumez committed
326 327 328
  def fileHTML(self, business_template, file_path):
    """ Display a file content in HTML with syntax highlighting
    """
329
    file_path = self._getWorkingPath(self.relativeToAbsolute(file_path, business_template))
330 331
    if os.path.exists(file_path):
      if os.path.isdir(file_path):
332
        text = "<span style='font-weight: bold; color: black;'>"+file_path+"</span><hr/>"
333
        text += file_path +" is a folder!"
334
      else:
335
        input_file = open(file_path, 'rU')
336
        head = "<span style='font-weight: bold; color: black;'>"+file_path+'</span>  <a href="' + \
Christophe Dumez's avatar
Christophe Dumez committed
337
        self.editPath(business_template, file_path) + \
Jérome Perrin's avatar
Jérome Perrin committed
338
        '"><img src="ERP5Subversion_imgs/edit.png" style="border: 0" alt="edit" /></a><hr/>'
Christophe Dumez's avatar
Christophe Dumez committed
339
        text = head + colorize(input_file.read())
340
        input_file.close()
341 342
    else:
      # see if tmp file is here (svn deleted file)
Christophe Dumez's avatar
Christophe Dumez committed
343 344
      if file_path[-1] == os.sep:
        file_path = file_path[:-1]
345 346
      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
347 348
      tmp_path = os.path.join(tmp_path, '.svn', 'text-base', \
      filename+'.svn-base')
349
      if os.path.exists(tmp_path):
350
        input_file = open(tmp_path, 'rU')
351
        head = "<span style='font-weight: bold'>"+tmp_path+"</span> (svn temporary file)<hr/>"
Christophe Dumez's avatar
Christophe Dumez committed
352
        text = head + colorize(input_file.read())
353
        input_file.close()
354
      else : # does not exist
355
        text = "<span style='font-weight: bold'>"+file_path+"</span><hr/>"
356
        text += file_path +" does not exist!"
357
    return text
358
      
Yoshinori Okuji's avatar
Yoshinori Okuji committed
359 360 361 362 363 364 365 366 367
  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
368
      trust_list.append(cookie)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
369 370 371 372
    # Set the cookie.
    response = request.RESPONSE
    trust_list.append(self._encodeSSLTrust(trust_dict, permanent))
    value = ','.join(trust_list)
373
    expires = (DateTime() + 1).toZone('GMT').rfc822()
374
    request.set(self.ssl_trust_cookie_name, value)
Christophe Dumez's avatar
Christophe Dumez committed
375 376
    response.setCookie(self.ssl_trust_cookie_name, value, path = '/', \
    expires = expires)
Christophe Dumez's avatar
Christophe Dumez committed
377 378
    
  def acceptSSLPerm(self, trust_dict):
Christophe Dumez's avatar
Christophe Dumez committed
379 380
    """ Accept SSL server permanently
    """
Christophe Dumez's avatar
Christophe Dumez committed
381
    self.acceptSSLServer(self, trust_dict, True)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398

  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)
399
  
400 401 402 403 404 405 406 407 408 409 410 411 412 413
  security.declareProtected('Import/Export objects', 'createSubversionPath')
  def createSubversionPath(self, working_copy, business_template):
    """
     create the working copy path corresponding to the given business
     template, checking it is in the working copy list in preferences
    """
    bt_name = business_template.getTitle()
    assert bt_name == os.path.basename(bt_name), 'Invalid bt_name'
    working_copy = self._getWorkingPath(working_copy)
    wc_path = os.path.join(working_copy, bt_name)
    os.mkdir(wc_path)
    client = self._getClient()
    client.add([wc_path])

414
  security.declareProtected('Import/Export objects', 'getSubversionPath')
Christophe Dumez's avatar
Christophe Dumez committed
415 416 417 418 419 420 421 422 423 424 425
  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()
426
    if not wc_list or len(wc_list) == 0 :
Christophe Dumez's avatar
Christophe Dumez committed
427 428 429 430 431 432 433
      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)
434 435 436 437 438
        if os.path.isdir(wc_path):
          if with_name:
            return wc_path
          else:
            return os.sep.join(wc_path.split(os.sep)[:-1])
439
    raise SubversionUnknownBusinessTemplateError, \
440
        "Could not find '%s' at first level of working copies." % (bt_name, )
441 442

  def _getWorkingPath(self, path):
Christophe Dumez's avatar
Christophe Dumez committed
443 444
    """ Check if the given path is reachable (allowed)
    """
445 446 447
    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
448 449
      raise UnauthorizedAccessToPath, 'Unauthorized access to path %s.' \
          'It is NOT in your Zope home instance.' % real_path
450
    return real_path
451
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
452
  security.declareProtected('Import/Export objects', 'update')
Christophe Dumez's avatar
Christophe Dumez committed
453
  def update(self, business_template):
454 455 456
    """
     Update a working copy / business template, 
     reverting all local changes
Yoshinori Okuji's avatar
Yoshinori Okuji committed
457
    """
Christophe Dumez's avatar
Christophe Dumez committed
458
    path = self._getWorkingPath(self.getSubversionPath(business_template))
459
    # First remove unversioned in working copy that could conflict
460
    self.removeAllInList([x['uid'] for x in self.unversionedFiles(path)])
Yoshinori Okuji's avatar
Yoshinori Okuji committed
461
    client = self._getClient()
462 463 464
    # Revert local changes in working copy first
    # to import a "pure" BT after update
    self.revert(path=path, recurse=True)
465
    # removed unversioned files due to former added files that were reverted
466
    self.removeAllInList([x['uid'] for x in self.unversionedFiles(path)])
467 468 469
    # Update from SVN
    client.update(path)
    # Import in zodb
Christophe Dumez's avatar
Christophe Dumez committed
470
    return self.importBT(business_template)
471 472 473 474 475
  
  security.declareProtected('Import/Export objects', 'updateKeep')
  def updateKeep(self, business_template):
    """
     Update a working copy / business template, 
Christophe Dumez's avatar
Christophe Dumez committed
476
     keeping all local changes (appart from revision)
477 478 479 480 481 482 483 484
    """
    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
485
    self.removeAllInList([x['uid'] for x in self.unversionedFiles(path)])
486 487 488 489 490 491 492 493
    # 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
494

495
  security.declareProtected('Import/Export objects', 'switch')
Christophe Dumez's avatar
Christophe Dumez committed
496
  def switch(self, business_template, url):
497 498
    """switch SVN repository for a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
499
    path = self._getWorkingPath(self.getSubversionPath(business_template))
500
    client = self._getClient()
501 502
    if url[-1] == '/' :
      url = url[:-1]
503
    # Update from SVN
504
    client.switch(path=path, url=url)
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524

  security.declareProtected('Import/Export objects', 'checkout')
  def checkout(self, business_template, url):
    """ Checkout business configuration from SVN into 
        the first Preferres Subversion Working Copy.
    """
    wc_list = self.getPortalObject().portal_preferences\
    .getPreferredSubversionWorkingCopyList()
    if not wc_list or len(wc_list) == 0 :
      raise SubversionPreferencesError, \
      'Please set at least one Subversion Working Copy in preferences first.'
    bt_name = business_template.getTitle()
    wc_path = os.path.join(wc_list[0], bt_name)
    path = self._getWorkingPath(wc_path)
    client = self._getClient()
    if url[-1] != '/' :
      url += '/' 
    url += bt_name
    client.checkout(path=path, url=url)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
525
  security.declareProtected('Import/Export objects', 'add')
526
  # path can be a list or not (relative or absolute)
Christophe Dumez's avatar
Christophe Dumez committed
527
  def add(self, path, business_template=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
528 529
    """Add a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
530
    if business_template is not None:
531
      if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
532 533
        path = [self._getWorkingPath(self.relativeToAbsolute(x, \
        business_template)) for x in path]
534
      else:
Christophe Dumez's avatar
Christophe Dumez committed
535 536
        path = self._getWorkingPath(self.relativeToAbsolute(path, \
        business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
537
    client = self._getClient()
538
    return client.add(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
539

540
  security.declareProtected('Import/Export objects', 'info')
Christophe Dumez's avatar
Christophe Dumez committed
541
  def info(self, business_template):
542 543
    """return info of working copy
    """
Christophe Dumez's avatar
Christophe Dumez committed
544 545
    working_copy = self._getWorkingPath(self\
    .getSubversionPath(business_template))
546 547 548
    client = self._getClient()
    return client.info(working_copy)
  
Christophe Dumez's avatar
Christophe Dumez committed
549
  security.declareProtected('Import/Export objects', 'log')
550
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
551
  def log(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
552 553 554
    """return log of a file or dir
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
555 556
    return client.log(self._getWorkingPath(self\
    .relativeToAbsolute(path, business_template)))
Christophe Dumez's avatar
Christophe Dumez committed
557
  
558
  security.declareProtected('Import/Export objects', 'cleanup')
Christophe Dumez's avatar
Christophe Dumez committed
559
  def cleanup(self, business_template):
560 561
    """remove svn locks in working copy
    """
Christophe Dumez's avatar
Christophe Dumez committed
562 563
    working_copy = self._getWorkingPath(self\
    .getSubversionPath(business_template))
564 565 566
    client = self._getClient()
    return client.cleanup(working_copy)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
567
  security.declareProtected('Import/Export objects', 'remove')
568
  # path can be a list or not (relative or absolute)
Christophe Dumez's avatar
Christophe Dumez committed
569
  def remove(self, path, business_template=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
570 571
    """Remove a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
572
    if business_template is not None:
573
      if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
574 575
        path = [self._getWorkingPath(self\
        .relativeToAbsolute(x, business_template)) for x in path]
576
      else:
Christophe Dumez's avatar
Christophe Dumez committed
577 578
        path = self._getWorkingPath(self.relativeToAbsolute(path, \
        business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
579
    client = self._getClient()
580
    return client.remove(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
581 582 583 584 585 586

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

Christophe Dumez's avatar
Christophe Dumez committed
589
  security.declareProtected('Import/Export objects', 'ls')
590
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
591
  def ls(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
592 593 594
    """Display infos about a file.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
595 596
    return client.ls(self._getWorkingPath(self\
    .relativeToAbsolute(path, business_template)))
Christophe Dumez's avatar
Christophe Dumez committed
597

Yoshinori Okuji's avatar
Yoshinori Okuji committed
598
  security.declareProtected('Import/Export objects', 'diff')
599
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
600
  def diff(self, path, business_template, revision1=None, revision2=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
601 602 603
    """Make a diff for a file or a directory.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
604 605
    return client.diff(self._getWorkingPath(self.relativeToAbsolute(path, \
    business_template)), revision1, revision2)
606
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
607
  security.declareProtected('Import/Export objects', 'revert')
608
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
609
  def revert(self, path, business_template=None, recurse=False):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
610 611 612
    """Revert local changes in a file or a directory.
    """
    client = self._getClient()
613
    if not isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
614 615 616 617 618
      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
619
    client.revert(path, recurse)
620 621 622

  security.declareProtected('Import/Export objects', 'revertZODB')
  # path can be absolute or relative
623 624 625 626 627 628 629 630 631
  def revertZODB(self,
      business_template,
      added_file_list=None,
      other_file_list=None,
      recurse=False,
      # deprecated:
      added_files=None,
      other_files=None,
      ):
632 633 634
    """Revert local changes in a file or a directory
       in ZODB and on hard drive
    """
635 636 637 638 639 640 641 642 643
    if added_files is not None:
      warn('Parameter added_files is deprecated, used added_file_list ' \
           'instead.', DeprecationWarning)
      added_file_list = added_files
    if other_files is not None:
      warn('Parameter other_files is deprecated, used other_file_list ' \
           'instead.', DeprecationWarning)
      other_file_list = other_files

644 645
    client = self._getClient()
    object_to_update = {}
Christophe Dumez's avatar
Christophe Dumez committed
646
    # Transform params to list if they are not already lists
647 648 649 650 651 652 653 654
    if not added_file_list:
      added_file_list = []
    if not other_file_list:
      other_file_list = []
    if not isinstance(added_file_list, list) :
      added_file_list = [added_file_list]
    if not isinstance(other_file_list, list) :
      other_file_list = [other_file_list]
655
      
656
    # Reinstall removed or modified files
657
    for path in other_file_list :
658 659 660
      # security check
      self._getWorkingPath(self.relativeToAbsolute(path, business_template))
      path_list = path.split(os.sep)
661 662 663 664
      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
665
          tmp = os.path.splitext(tmp)[0]
666
          object_to_update[tmp] = 'install'
667
    path_added_list = []
668
    # remove added files
669
    for path in added_file_list :
670 671 672
      # security check
      self._getWorkingPath(self.relativeToAbsolute(path, business_template))
      path_list = path.split(os.sep)
673 674 675 676
      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
677 678
          tmp = os.path.splitext(tmp)[0]
          path_added_list.append(tmp)
679 680
    ## hack to remove objects
    # Create a temporary bt with objects to delete
681
    tmp_bt = getToolByName(business_template.getPortalObject(), 'portal_templates')\
Christophe Dumez's avatar
Christophe Dumez committed
682
    .newContent(portal_type="Business Template")
683 684 685 686 687 688 689 690 691
    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
692
    business_template.portal_templates.manage_delObjects(ids=tmp_bt.getId())
693
    #revert changes
694
    added_file_list.extend(other_file_list)
Christophe Dumez's avatar
Christophe Dumez committed
695
    to_revert = [self.relativeToAbsolute(x, business_template) \
696
    for x in added_file_list]
697 698 699
    if len(to_revert) != 0 :
      client.revert(to_revert, recurse)
      # Partially reinstall installed bt
Christophe Dumez's avatar
Christophe Dumez committed
700 701
      installed_bt = business_template.portal_templates\
      .getInstalledBusinessTemplate(business_template.getTitle())
702 703
      if installed_bt is None:
        raise SubversionBusinessTemplateNotInstalled, "Revert won't work if the business template is not installed. Please install it first."
704
      installed_bt.reinstall(object_to_update=object_to_update, force=0)
Christophe Dumez's avatar
Christophe Dumez committed
705 706 707
    
  security.declareProtected('Import/Export objects', 'resolved')
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
708
  def resolved(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
709 710 711 712
    """remove conflicted status
    """
    client = self._getClient()
    if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
713 714
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
Christophe Dumez's avatar
Christophe Dumez committed
715
    else:
Christophe Dumez's avatar
Christophe Dumez committed
716 717
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
Christophe Dumez's avatar
Christophe Dumez committed
718
    return client.resolved(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
719

Christophe Dumez's avatar
Christophe Dumez committed
720 721 722 723
  def relativeToAbsolute(self, path, business_template):
    """ Return an absolute path given the absolute one
        and the business template
    """
724 725 726 727
    if path[0] == os.sep:
      # already absolute
      return path
    # relative path
Christophe Dumez's avatar
Christophe Dumez committed
728 729 730
    if path.split(os.sep)[0] == business_template.getTitle():
      return os.path.join(self.getSubversionPath(business_template, \
      False), path)
731
    else:
Christophe Dumez's avatar
Christophe Dumez committed
732
      return os.path.join(self.getSubversionPath(business_template), path)
733

Yoshinori Okuji's avatar
Yoshinori Okuji committed
734
  security.declareProtected('Import/Export objects', 'checkin')
735
  # path can be relative or absolute (can be a list of paths too)
Christophe Dumez's avatar
Christophe Dumez committed
736
  def checkin(self, path, business_template, log_message=None, recurse=True):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
737 738
    """Commit local changes.
    """
739
    if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
740 741
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
742
    else:
Christophe Dumez's avatar
Christophe Dumez committed
743 744
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
745
    client = self._getClient()
746 747 748
    # Pysvn wants unicode objects
    if isinstance(log_message, str):
      log_message = log_message.decode('utf8')
749
    return client.checkin(path, log_message, recurse)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
750

751
  security.declareProtected('Import/Export objects', 'getLastChangelog')
Christophe Dumez's avatar
Christophe Dumez committed
752
  def getLastChangelog(self, business_template):
753 754
    """Return last changelog of a business template
    """
Christophe Dumez's avatar
Christophe Dumez committed
755
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template))
756
    changelog_path = bt_path + os.sep + 'bt' + os.sep + 'change_log'
Christophe Dumez's avatar
Christophe Dumez committed
757
    changelog = ""
758 759 760 761 762 763 764
    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
765
        changelog += line
766 767 768
    return changelog
    

Yoshinori Okuji's avatar
Yoshinori Okuji committed
769 770 771 772 773
  security.declareProtected('Import/Export objects', 'status')
  def status(self, path, **kw):
    """Get status.
    """
    client = self._getClient()
774
    return client.status(self._getWorkingPath(path), **kw)
775
  
776 777 778 779 780
  security.declareProtected('Import/Export objects', 'unversionedFiles')
  def unversionedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
781
    status_list = client.status(self._getWorkingPath(path), **kw)
782
    unversioned_list = []
Christophe Dumez's avatar
Christophe Dumez committed
783 784
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "unversioned":
785
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
786
        my_dict['uid'] = status_obj.getPath()
787 788 789
        unversioned_list.append(my_dict)
    return unversioned_list
      
790 791 792 793 794
  security.declareProtected('Import/Export objects', 'conflictedFiles')
  def conflictedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
795
    status_list = client.status(self._getWorkingPath(path), **kw)
796
    conflicted_list = []
Christophe Dumez's avatar
Christophe Dumez committed
797 798
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "conflicted":
799
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
800
        my_dict['uid'] = status_obj.getPath()
801 802 803
        conflicted_list.append(my_dict)
    return conflicted_list

804
  security.declareProtected('Import/Export objects', 'removeAllInList')
805
  def removeAllInList(self, path_list, REQUEST=None):
806 807
    """Remove all files and folders in list
    """
808 809 810
    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
811
    for file_path in path_list:
812 813
      real_path = self._getWorkingPath(file_path)
      if os.path.isdir(real_path):
814
        shutil.rmtree(real_path)
815 816
      elif os.path.isfile(real_path):
        os.remove(real_path)
817
    
Christophe Dumez's avatar
Christophe Dumez committed
818 819 820
  def getModifiedTree(self, business_template, show_unmodified=False) :
    """ Return tree of files returned by svn status
    """
821
    # Get subversion path without business template name at the end
Christophe Dumez's avatar
Christophe Dumez committed
822 823
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template, \
    False))
824 825
    if bt_path[-1] != os.sep:
      bt_path += os.sep
826
    # Business template root directory is the root of the tree
Christophe Dumez's avatar
Christophe Dumez committed
827 828
    root = Dir(business_template.getTitle(), "normal")
    something_modified = False
829
    
830
    statusObj_list = self.status(os.path.join(bt_path, \
831
    business_template.getTitle()), update=False)
832
    # We browse the files returned by svn status
833
    for status_obj in statusObj_list :
834
      # can be (normal, added, modified, deleted, conflicted, unversioned)
835
      status = str(status_obj.getTextStatus())
836
      if str(status_obj.getReposTextStatus()) != 'none':
Jérome Perrin's avatar
Jérome Perrin committed
837
        status = "outdated"
838
      if (show_unmodified or status != "normal") and status != "unversioned":
Christophe Dumez's avatar
Christophe Dumez committed
839
        something_modified = True
840 841 842 843 844 845
        # 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
846
        parent = root
Christophe Dumez's avatar
Christophe Dumez committed
847
        
848 849
        # 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
850 851 852 853 854
        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)
855 856 857
        
        # 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
858
        if os.path.isdir(full_path) :
859 860 861 862 863
          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
864
          else :
865 866 867
            # update msg status
            tmp = parent.getDirFromName(filename)
            tmp.status = str(status)
Christophe Dumez's avatar
Christophe Dumez committed
868
        else :
869 870
          # add new file to the tree
          parent.sub_files.append(File(filename, str(status)))
Christophe Dumez's avatar
Christophe Dumez committed
871
    return something_modified and root
872
  
Christophe Dumez's avatar
Christophe Dumez committed
873 874 875 876 877 878
  def extractBT(self, business_template):
    """ 
     Extract business template to hard drive
     and do svn add/del stuff comparing it
     to local working copy
    """
879 880
    if business_template.getBuildingState() == 'draft':
      business_template.edit()
Christophe Dumez's avatar
Christophe Dumez committed
881 882 883
    business_template.build()
    svn_path = self._getWorkingPath(self.getSubversionPath(business_template) \
    + os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
884
    path = mktemp() + os.sep
885
    try:
886
      # XXX: Big hack to make export work as expected.
887
      transaction.commit()
Christophe Dumez's avatar
Christophe Dumez committed
888
      business_template.export(path=path, local=1)
889
      # svn del deleted files
Christophe Dumez's avatar
Christophe Dumez committed
890
      self.deleteOldFiles(svn_path, path)
891
      # add new files and copy
Christophe Dumez's avatar
Christophe Dumez committed
892 893 894
      self.addNewFiles(svn_path, path)
      self.goToWorkingCopy(business_template)
    except (pysvn.ClientError, NotFound, AttributeError, \
895
    Error), error:
896
      # Clean up
897
      shutil.rmtree(path)
898
      raise error
899
    # Clean up
Christophe Dumez's avatar
Christophe Dumez committed
900
    self.activate().removeAllInList([path, ])
901
    
Christophe Dumez's avatar
Christophe Dumez committed
902 903 904 905 906 907 908
  def importBT(self, business_template):
    """
     Import business template from local
     working copy
    """
    return business_template.download(self._getWorkingPath(self\
    .getSubversionPath(business_template)))
909
    
910 911
  # Get a list of files and keep only parents
  # Necessary before recursively commit removals
Christophe Dumez's avatar
Christophe Dumez committed
912 913 914 915 916 917
  def cleanChildrenInList(self, path_list):
    """
     Get a list of files and keep only parents
     Necessary before recursively commit removals
    """
    res = path_list
918 919 920
    for path in path_list:
      path = path + '/'
      res = [x for x in res if not x.startswith(path)]
921
    return res
922

923 924
  # return a set with directories present in the directory
  def getSetDirsForDir(self, directory):
925
    dir_set = set()
Christophe Dumez's avatar
Christophe Dumez committed
926
    for root, dirs, _ in cacheWalk(directory):
927 928 929 930 931
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
      # get Directories
      for name in dirs:
932
        i = root.replace(directory, '').count(os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
933 934
        path = os.path.join(root, name)
        dir_set.add((i, path.replace(directory,'')))
935 936 937 938 939
    return dir_set
      
  # return a set with files present in the directory
  def getSetFilesForDir(self, directory):
    dir_set = set()
940
    for root, dirs, files in cacheWalk(directory):
941 942 943
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
944
      # get Files
945 946
      for name in files:
        i = root.replace(directory, '').count(os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
947 948
        path = os.path.join(root, name)
        dir_set.add((i, path.replace(directory,'')))
949
    return dir_set
950
  
951
  # return files present in new_dir but not in old_dir
952 953
  # return a set of relative paths
  def getNewFiles(self, old_dir, new_dir):
954 955 956 957
    if old_dir[-1] != os.sep:
      old_dir += os.sep
    if new_dir[-1] != os.sep:
      new_dir += os.sep
958 959
    old_set = self.getSetFilesForDir(old_dir)
    new_set = self.getSetFilesForDir(new_dir)
960 961
    return new_set.difference(old_set)

962 963 964 965 966 967 968 969 970 971 972
  # 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
973 974 975
  def deleteOldFiles(self, old_dir, new_dir):
    """ svn del files that have been removed in new dir
    """
976
    # detect removed files
977
    files_set = self.getNewFiles(new_dir, old_dir)
978 979
    # detect removed directories
    dirs_set = self.getNewDirs(new_dir, old_dir)
980
    # svn del
Christophe Dumez's avatar
Christophe Dumez committed
981 982 983 984 985 986
    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])
987
  
Christophe Dumez's avatar
Christophe Dumez committed
988 989 990
  def addNewFiles(self, old_dir, new_dir):
    """ copy files and add new files
    """
991
    # detect created files
992
    files_set = self.getNewFiles(old_dir, new_dir)
993 994
    # detect created directories
    dirs_set = self.getNewDirs(old_dir, new_dir)
995
    # Copy files
996
    copytree(new_dir, old_dir)
997
    # svn add
Christophe Dumez's avatar
Christophe Dumez committed
998 999 1000 1001 1002 1003
    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])
1004
  
Christophe Dumez's avatar
Christophe Dumez committed
1005 1006 1007
  def treeToXML(self, item, business_template) :
    """ Convert tree in memory to XML
    """
1008
    output = '<?xml version="1.0" encoding="UTF-8"?>'+ os.linesep
1009
    output += "<tree id='0'>" + os.linesep
Christophe Dumez's avatar
Christophe Dumez committed
1010
    output = self._treeToXML(item, output, business_template.getTitle(), True)
1011
    output += '</tree>' + os.linesep
1012
    return output
1013
  
1014
  def _treeToXML(self, item, output, relative_path, first) :
Christophe Dumez's avatar
Christophe Dumez committed
1015 1016 1017 1018
    """
     Private function to convert recursively tree 
     in memory to XML
    """
1019
    # Choosing a color coresponding to the status
1020 1021 1022 1023 1024 1025 1026 1027 1028
    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'
1029 1030
    elif status == 'outdated' :
      color = 'purple'
Christophe Dumez's avatar
Christophe Dumez committed
1031
    else :
1032
      color = 'black'
1033
    if isinstance(item, Dir) :
Christophe Dumez's avatar
Christophe Dumez committed
1034
      if first :
1035
        output += '<item open="1" text="%s" id="%s" aCol="%s" '\
Christophe Dumez's avatar
Christophe Dumez committed
1036
        'im0="folder.png" im1="folder_open.png" '\
1037
        'im2="folder.png">'%(item.name, relative_path, color) + os.linesep
1038
        first = False
Christophe Dumez's avatar
Christophe Dumez committed
1039
      else :
1040
        output += '<item text="%s" id="%s" aCol="%s" im0="folder.png" ' \
1041 1042 1043
        '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
1044 1045
        output = self._treeToXML(item.getObjectFromName(it.name), output, \
        os.path.join(relative_path,it.name),first)
1046
      output += '</item>' + os.linesep
1047
    else :
1048
      output += '<item text="%s" id="%s" aCol="%s" im0="document.png"/>'\
1049
                %(item.name, relative_path, color) + os.linesep
1050
    return output
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1051 1052
    
InitializeClass(SubversionTool)