SubversionTool.py 38.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, mkdtemp
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
import transaction
Aurel's avatar
Aurel committed
50

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

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

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

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

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

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

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

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

133 134
class SubversionSecurityError(Exception): pass

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

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

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

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

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

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

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

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

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

  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)
397
  
398 399 400 401 402 403 404 405 406 407 408 409 410 411
  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])

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

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

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

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

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

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

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

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

Yoshinori Okuji's avatar
Yoshinori Okuji committed
596
  security.declareProtected('Import/Export objects', 'diff')
597
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
598
  def diff(self, path, business_template, revision1=None, revision2=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
599 600 601
    """Make a diff for a file or a directory.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
602 603
    return client.diff(self._getWorkingPath(self.relativeToAbsolute(path, \
    business_template)), revision1, revision2)
604
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
605
  security.declareProtected('Import/Export objects', 'revert')
606
  # path can be absolute or relative
607 608
  def revert(self, path, business_template=None,
                   recurse=False, exclude_list=()):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
609 610 611
    """Revert local changes in a file or a directory.
    """
    client = self._getClient()
612
    if isinstance(path, basestring):
613
      path = [path]
Christophe Dumez's avatar
Christophe Dumez committed
614
    if business_template is not None:
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632
      path = [self._getWorkingPath(self.relativeToAbsolute(x,
        business_template)) for x in path]
    if recurse and exclude_list:
      exclude_list = frozenset(self._getWorkingPath(self.relativeToAbsolute(x,
        business_template)) for x in exclude_list)
      added_set = set()
      other_list = []
      for path in path:
        for status in client.status(path):
          path = status.getPath()
          if path not in exclude_list:
            status = str(status.getTextStatus())
            if status == 'added':
              added_set.add(path)
            elif status != 'normal':
              other_list.append(path)
      client.revert(other_list, False)
      path = [x for x in added_set if os.path.dirname(x) not in added_set]
Christophe Dumez's avatar
Christophe Dumez committed
633
    client.revert(path, recurse)
634 635 636

  security.declareProtected('Import/Export objects', 'revertZODB')
  # path can be absolute or relative
637 638 639 640 641 642 643 644 645
  def revertZODB(self,
      business_template,
      added_file_list=None,
      other_file_list=None,
      recurse=False,
      # deprecated:
      added_files=None,
      other_files=None,
      ):
646 647 648
    """Revert local changes in a file or a directory
       in ZODB and on hard drive
    """
649 650 651 652 653 654 655 656 657
    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

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

Christophe Dumez's avatar
Christophe Dumez committed
734 735 736 737
  def relativeToAbsolute(self, path, business_template):
    """ Return an absolute path given the absolute one
        and the business template
    """
738 739 740 741
    if path[0] == os.sep:
      # already absolute
      return path
    # relative path
Christophe Dumez's avatar
Christophe Dumez committed
742 743 744
    if path.split(os.sep)[0] == business_template.getTitle():
      return os.path.join(self.getSubversionPath(business_template, \
      False), path)
745
    else:
Christophe Dumez's avatar
Christophe Dumez committed
746
      return os.path.join(self.getSubversionPath(business_template), path)
747

Yoshinori Okuji's avatar
Yoshinori Okuji committed
748
  security.declareProtected('Import/Export objects', 'checkin')
749
  # path can be relative or absolute (can be a list of paths too)
Christophe Dumez's avatar
Christophe Dumez committed
750
  def checkin(self, path, business_template, log_message=None, recurse=True):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
751 752
    """Commit local changes.
    """
753
    if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
754 755
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
756
    else:
Christophe Dumez's avatar
Christophe Dumez committed
757 758
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
759
    client = self._getClient()
760 761 762
    # Pysvn wants unicode objects
    if isinstance(log_message, str):
      log_message = log_message.decode('utf8')
763
    return client.checkin(path, log_message, recurse)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
764

765
  security.declareProtected('Import/Export objects', 'getLastChangelog')
Christophe Dumez's avatar
Christophe Dumez committed
766
  def getLastChangelog(self, business_template):
767 768
    """Return last changelog of a business template
    """
Christophe Dumez's avatar
Christophe Dumez committed
769
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template))
770
    changelog_path = bt_path + os.sep + 'bt' + os.sep + 'change_log'
Christophe Dumez's avatar
Christophe Dumez committed
771
    changelog = ""
772 773 774 775 776 777 778
    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
779
        changelog += line
780 781 782
    return changelog
    

Yoshinori Okuji's avatar
Yoshinori Okuji committed
783 784 785 786 787
  security.declareProtected('Import/Export objects', 'status')
  def status(self, path, **kw):
    """Get status.
    """
    client = self._getClient()
788
    return client.status(self._getWorkingPath(path), **kw)
789
  
790 791 792 793 794
  security.declareProtected('Import/Export objects', 'unversionedFiles')
  def unversionedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
795
    status_list = client.status(self._getWorkingPath(path), **kw)
796
    unversioned_list = []
Christophe Dumez's avatar
Christophe Dumez committed
797 798
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "unversioned":
799
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
800
        my_dict['uid'] = status_obj.getPath()
801 802 803
        unversioned_list.append(my_dict)
    return unversioned_list
      
804 805 806 807 808
  security.declareProtected('Import/Export objects', 'conflictedFiles')
  def conflictedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
809
    status_list = client.status(self._getWorkingPath(path), **kw)
810
    conflicted_list = []
Christophe Dumez's avatar
Christophe Dumez committed
811 812
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "conflicted":
813
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
814
        my_dict['uid'] = status_obj.getPath()
815 816 817
        conflicted_list.append(my_dict)
    return conflicted_list

818
  security.declareProtected('Import/Export objects', 'removeAllInList')
819
  def removeAllInList(self, path_list, REQUEST=None):
820 821
    """Remove all files and folders in list
    """
822 823 824
    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
825
    for file_path in path_list:
826 827
      real_path = self._getWorkingPath(file_path)
      if os.path.isdir(real_path):
828
        shutil.rmtree(real_path)
829 830
      elif os.path.isfile(real_path):
        os.remove(real_path)
831
    
Christophe Dumez's avatar
Christophe Dumez committed
832 833 834
  def getModifiedTree(self, business_template, show_unmodified=False) :
    """ Return tree of files returned by svn status
    """
835
    # Get subversion path without business template name at the end
Christophe Dumez's avatar
Christophe Dumez committed
836 837
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template, \
    False))
838 839
    if bt_path[-1] != os.sep:
      bt_path += os.sep
840
    # Business template root directory is the root of the tree
Christophe Dumez's avatar
Christophe Dumez committed
841 842
    root = Dir(business_template.getTitle(), "normal")
    something_modified = False
843 844 845 846 847 848 849 850
    template_tool = self.getPortalObject().portal_templates
    if template_tool.getDiffFilterScriptList():
      client_diff = self._getClient().diff
      def hasDiff(path):
        return template_tool.getFilteredDiff(client_diff(path, None, None))
    else:
      def hasDiff(path):
        return True
851
    statusObj_list = self.status(os.path.join(bt_path, \
852
    business_template.getTitle()), update=False)
853
    # We browse the files returned by svn status
854
    for status_obj in statusObj_list :
855
      # can be (normal, added, modified, deleted, conflicted, unversioned)
856
      if str(status_obj.getReposTextStatus()) != 'none':
Jérome Perrin's avatar
Jérome Perrin committed
857
        status = "outdated"
858 859 860 861 862 863 864
      else:
        status = str(status_obj.getTextStatus())
        if status == "unversioned" or \
           status == "normal" and not show_unmodified:
          continue
      full_path = status_obj.getPath()
      if status != "modified" or hasDiff(full_path):
Christophe Dumez's avatar
Christophe Dumez committed
865
        something_modified = True
866 867 868 869 870
        # Get object path
        relative_path = full_path.replace(bt_path, '')
        filename = os.path.basename(relative_path)

        # Always start from root
871
        parent = root
Christophe Dumez's avatar
Christophe Dumez committed
872
        
873 874
        # 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
875 876 877 878 879
        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)
880 881 882
        
        # 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
883
        if os.path.isdir(full_path) :
884 885 886 887 888
          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
889
          else :
890 891 892
            # update msg status
            tmp = parent.getDirFromName(filename)
            tmp.status = str(status)
Christophe Dumez's avatar
Christophe Dumez committed
893
        else :
894 895
          # add new file to the tree
          parent.sub_files.append(File(filename, str(status)))
Christophe Dumez's avatar
Christophe Dumez committed
896
    return something_modified and root
897

Christophe Dumez's avatar
Christophe Dumez committed
898 899 900 901 902 903
  def extractBT(self, business_template):
    """ 
     Extract business template to hard drive
     and do svn add/del stuff comparing it
     to local working copy
    """
904 905
    if business_template.getBuildingState() == 'draft':
      business_template.edit()
Christophe Dumez's avatar
Christophe Dumez committed
906 907 908
    business_template.build()
    svn_path = self._getWorkingPath(self.getSubversionPath(business_template) \
    + os.sep)
909
    old_cwd = os.getcwd()
910
    path = mkdtemp()
911
    try:
912
      # XXX: Big hack to make export work as expected.
913
      transaction.commit()
Christophe Dumez's avatar
Christophe Dumez committed
914 915
      business_template.export(path=path, local=1)
      self.goToWorkingCopy(business_template)
916
      self._reimportTree(path)
917
    finally:
918
      # Clean up
919
      shutil.rmtree(path)
920 921
      os.chdir(old_cwd)

Christophe Dumez's avatar
Christophe Dumez committed
922 923 924 925 926 927 928
  def importBT(self, business_template):
    """
     Import business template from local
     working copy
    """
    return business_template.download(self._getWorkingPath(self\
    .getSubversionPath(business_template)))
929
    
930 931
  # Get a list of files and keep only parents
  # Necessary before recursively commit removals
Christophe Dumez's avatar
Christophe Dumez committed
932 933 934 935 936 937
  def cleanChildrenInList(self, path_list):
    """
     Get a list of files and keep only parents
     Necessary before recursively commit removals
    """
    res = path_list
938 939 940
    for path in path_list:
      path = path + '/'
      res = [x for x in res if not x.startswith(path)]
941
    return res
942

943 944
  def _reimportTree(self, path):
    """Overwrite working copy with the tree pointed to by 'path'
945

946
    Current directory must be the destination working copy
Christophe Dumez's avatar
Christophe Dumez committed
947
    """
948 949
    client = self._getClient()
    # Dicts to track svn status in case it is not consistent with existing
950
    # files and directories
951
    versioned_dict = dict((x.getPath(), x) for x in client.status('.')
952
      if str(x.getTextStatus()) not in ('ignored', 'unversioned'))
953
    del versioned_dict['.']
954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978
    added_set = set()

    # Walk current tree
    svn_file_set = set()
    svn_dir_set = set()
    prefix_length = len(os.path.join('.', ''))
    for dirpath, dirnames, filenames in os.walk('.'):
      dirpath = dirpath[prefix_length:]
      for i in xrange(len(dirnames) - 1, -1, -1):
        d = dirnames[i]
        if d[0] == '.':
          # Ignore hidden directories (in particular '.svn')
          del dirnames[i]
        else:
          svn_dir_set.add(os.path.join(dirpath, d))
      for f in filenames:
        svn_file_set.add(os.path.join(dirpath, f))

    # Copy new files/dirs from 'path' to working copy
    # Note: we don't use 'copytree' to avoid excessive disk writes
    prefix_length = len(os.path.join(path, ''))
    for dirpath, dirnames, filenames in os.walk(path):
      dirpath = dirpath[prefix_length:]
      for d in dirnames:
        d = os.path.join(dirpath, d)
979 980
        status = versioned_dict.pop(d, None)
        if status is None:
981
          added_set.add(d)
982 983
        elif str(status.getTextStatus()) == 'deleted':
          client.revert(d)
984 985 986 987 988 989
        if d in svn_dir_set:
          svn_dir_set.remove(d)
        else:
          os.mkdir(d)
      for f in filenames:
        f = os.path.join(dirpath, f)
990 991
        status = versioned_dict.pop(f, None)
        if status is None:
992
          added_set.add(f)
993 994 995
        elif str(status.getTextStatus()) == 'deleted':
          client.revert(f)
          svn_file_set.add(f)
996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017
        # copy file unless unchanged
        file = open(os.path.join(path, f), 'rb')
        try:
          text = file.read()
        finally:
          file.close()
        try:
          if f in svn_file_set:
            svn_file_set.remove(f)
            file = open(f, 'r+b')
            old_size = os.fstat(file.fileno()).st_size
            if len(text) == old_size and text == file.read():
              continue
            file.seek(0)
          else:
            file = open(f, 'wb')
          file.write(text)
          file.truncate()
        finally:
          file.close()

    # Remove dangling files/dirs
1018 1019
    svn_file_set.difference_update(versioned_dict) # what is in versioned_dict
    svn_dir_set.difference_update(versioned_dict)  #  is removed after
1020 1021 1022 1023 1024 1025 1026 1027
    for x in svn_file_set:
      if os.path.dirname(x) not in svn_dir_set:
        os.remove(x)
    for x in svn_dir_set:
      if os.path.dirname(x) not in svn_dir_set:
        shutil.rmtree(x)

    # Remove deleted files/dirs
1028 1029
    client.remove([x for x in versioned_dict
                     if os.path.dirname(x) not in versioned_dict])
1030
    # Add new files/dirs
1031 1032
    client.add([x for x in added_set
                  if os.path.dirname(x) not in added_set])
1033

Christophe Dumez's avatar
Christophe Dumez committed
1034 1035 1036
  def treeToXML(self, item, business_template) :
    """ Convert tree in memory to XML
    """
1037
    output = '<?xml version="1.0" encoding="UTF-8"?>'+ os.linesep
1038
    output += "<tree id='0'>" + os.linesep
Christophe Dumez's avatar
Christophe Dumez committed
1039
    output = self._treeToXML(item, output, business_template.getTitle(), True)
1040
    output += '</tree>' + os.linesep
1041
    return output
1042
  
1043
  def _treeToXML(self, item, output, relative_path, first) :
Christophe Dumez's avatar
Christophe Dumez committed
1044 1045 1046 1047
    """
     Private function to convert recursively tree 
     in memory to XML
    """
1048
    # Choosing a color coresponding to the status
1049 1050 1051 1052 1053 1054 1055 1056 1057
    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'
1058 1059
    elif status == 'outdated' :
      color = 'purple'
Christophe Dumez's avatar
Christophe Dumez committed
1060
    else :
1061
      color = 'black'
1062
    if isinstance(item, Dir) :
Christophe Dumez's avatar
Christophe Dumez committed
1063
      if first :
1064
        output += '<item open="1" text="%s" id="%s" aCol="%s" '\
Christophe Dumez's avatar
Christophe Dumez committed
1065
        'im0="folder.png" im1="folder_open.png" '\
1066
        'im2="folder.png">'%(item.name, relative_path, color) + os.linesep
1067
        first = False
Christophe Dumez's avatar
Christophe Dumez committed
1068
      else :
1069
        output += '<item text="%s" id="%s" aCol="%s" im0="folder.png" ' \
1070 1071 1072
        '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
1073 1074
        output = self._treeToXML(item.getObjectFromName(it.name), output, \
        os.path.join(relative_path,it.name),first)
1075
      output += '</item>' + os.linesep
1076
    else :
1077
      output += '<item text="%s" id="%s" aCol="%s" im0="document.png"/>'\
1078
                %(item.name, relative_path, color) + os.linesep
1079
    return output
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1080 1081
    
InitializeClass(SubversionTool)