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

from Products.CMFCore.utils import UniqueObject
31
from Products.ERP5Type.Tool.BaseTool import BaseTool
Yoshinori Okuji's avatar
Yoshinori Okuji committed
32 33 34 35 36 37
from AccessControl import ClassSecurityInfo
from Globals import InitializeClass, DTMLFile
from Products.ERP5Type.Document.Folder import Folder
from Products.ERP5Type import Permissions
from Products.ERP5Subversion import _dtmldir
from Products.ERP5Subversion.SubversionClient import newSubversionClient
38
import os, re
Yoshinori Okuji's avatar
Yoshinori Okuji committed
39 40 41
from DateTime import DateTime
from cPickle import dumps, loads
from App.config import getConfiguration
42
from tempfile import mktemp
43
from Products.CMFCore.utils import getToolByName
Christophe Dumez's avatar
Christophe Dumez committed
44
from Products.ERP5.Document.BusinessTemplate import removeAll
45
from xml.sax.saxutils import escape
46
from dircache import listdir
47
from OFS.Traversable import NotFound
48
from Products.ERP5Type.patches.copyTree import copytree, Error
49
from Products.ERP5Type.patches.cacheWalk import cacheWalk
Aurel's avatar
Aurel committed
50

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

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

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

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

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

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

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

136 137 138 139 140
class UnauthorizedAccessToPath(Exception):
  """ When path is not in zope home instance
  """
  pass

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

class DiffFile:
Christophe Dumez's avatar
Christophe Dumez committed
185
  """
186
  # Members :
Christophe Dumez's avatar
Christophe Dumez committed
187 188 189 190 191
   - path : path of the modified file
   - children : sub codes modified
   - old_revision
   - new_revision
  """
192

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

class CodeBlock:
Christophe Dumez's avatar
Christophe Dumez committed
277 278 279 280 281 282 283 284 285 286 287
  """
   A code block contains several SubCodeBlocks
   Members :
   - old_line : line in old code (before modif)
   - new line : line in new code (after modif)
  
   Methods :
   - getOldCodeList() : return code before modif
   - getNewCodeList() : return code after modif
   Note: the code returned is a list of tuples (code line, background color)
  """
288

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

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

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

  # Filter content (ZMI))
  def __init__(self):
Christophe Dumez's avatar
Christophe Dumez committed
475 476
    return Folder.__init__(self, SubversionTool.id)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
477 478

  def filtered_meta_types(self, user=None):
Christophe Dumez's avatar
Christophe Dumez committed
479 480 481 482 483 484 485 486 487 488
    """
     Filter content (ZMI))
     Filters the list of available meta types.
    """
    all = SubversionTool.inheritedAttribute('filtered_meta_types')(self)
    meta_types = []
    for meta_type in self.all_meta_types():
      if meta_type['name'] in self.allowed_types:
        meta_types.append(meta_type)
    return meta_types
Yoshinori Okuji's avatar
Yoshinori Okuji committed
489
    
Christophe Dumez's avatar
Christophe Dumez committed
490
  # path is the path in svn working copy
491 492
  # return edit_path in zodb to edit it
  # return '#' if no zodb path is found
Christophe Dumez's avatar
Christophe Dumez committed
493
  def editPath(self, business_template, path):
Christophe Dumez's avatar
Christophe Dumez committed
494
    """Return path to edit file
495
       path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
496
    """
Christophe Dumez's avatar
Christophe Dumez committed
497
    path = self.relativeToAbsolute(path, business_template).replace('\\', '/')
498
    if 'bt' in path.split('/'):
499
      # not in zodb
Christophe Dumez's avatar
Christophe Dumez committed
500
      return '#'
501 502 503
    # if file have been deleted then not in zodb
    if not os.path.exists(path):
      return '#'
Christophe Dumez's avatar
Christophe Dumez committed
504
    svn_path = self.getSubversionPath(business_template).replace('\\', '/')
Christophe Dumez's avatar
Christophe Dumez committed
505 506
    edit_path = path.replace(svn_path, '').strip()
    if edit_path == '':
507 508
      # not in zodb 
      return '#'
509
    if edit_path[0] == '/':
510
      edit_path = edit_path[1:]
Christophe Dumez's avatar
Christophe Dumez committed
511 512
    edit_path = '/'.join(edit_path.split('/')[1:]).strip()
    if edit_path == '':
513 514
      # not in zodb 
      return '#'
515
    # remove file extension
Christophe Dumez's avatar
Christophe Dumez committed
516 517
    edit_path = os.path.splitext(edit_path)[0]
    # Add beginning and end of url
Christophe Dumez's avatar
Christophe Dumez committed
518 519
    edit_path = os.path.join(business_template.REQUEST["BASE2"], \
    edit_path, 'manage_main')
Christophe Dumez's avatar
Christophe Dumez committed
520 521
    return edit_path
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
522
  def _encodeLogin(self, realm, user, password):
Christophe Dumez's avatar
Christophe Dumez committed
523 524
    """ Encode login information.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
525 526 527
    return b64encode(dumps((realm, user, password)))

  def _decodeLogin(self, login):
Christophe Dumez's avatar
Christophe Dumez committed
528 529
    """ Decode login information.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
530
    return loads(b64decode(login))
531
  
Christophe Dumez's avatar
Christophe Dumez committed
532 533 534 535 536
  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
537
    
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
  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)
553
    expires = (DateTime() + 1).toZone('GMT').rfc822()
554
    request.set(self.login_cookie_name, value)
Christophe Dumez's avatar
Christophe Dumez committed
555 556
    response.setCookie(self.login_cookie_name, value, path = '/', \
    expires = expires)
557

Yoshinori Okuji's avatar
Yoshinori Okuji committed
558 559 560 561 562 563 564 565 566
  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
567
      
Christophe Dumez's avatar
Christophe Dumez committed
568 569
  def getHeader(self, business_template, file_path):
    file_path = self.relativeToAbsolute(file_path, business_template)
570 571
    header = '<b><a href="BusinessTemplate_viewSvnShowFile?file=' + \
    file_path + '">' + file_path + '</a></b>'
Christophe Dumez's avatar
Christophe Dumez committed
572
    edit_path = self.editPath(business_template, file_path)
573
    if edit_path != '#':
574 575
      header += '&nbsp;&nbsp;<a href="'+self.editPath(business_template, \
      file_path) + '"><img src="imgs/edit.png" border="0"></a>'
576
    return header
Yoshinori Okuji's avatar
Yoshinori Okuji committed
577 578 579 580 581 582 583 584 585 586

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

  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)
680 681
  
  security.declareProtected('Import/Export objects', 'getSubversionPath')
Christophe Dumez's avatar
Christophe Dumez committed
682 683 684 685 686 687 688 689 690 691 692
  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()
693
    if not wc_list:
Christophe Dumez's avatar
Christophe Dumez committed
694 695
      wc_list = self.getPortalObject().portal_preferences.\
      default_site_preference.getPreferredSubversionWorkingCopyList()
696
      if not wc_list:
Christophe Dumez's avatar
Christophe Dumez committed
697 698
        raise SubversionPreferencesError, \
        'Please set at least one Subversion Working Copy in preferences first.'
699
    if len(wc_list) == 0 :
Christophe Dumez's avatar
Christophe Dumez committed
700 701 702 703 704 705 706
      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)
707 708 709 710 711
        if os.path.isdir(wc_path):
          if with_name:
            return wc_path
          else:
            return os.sep.join(wc_path.split(os.sep)[:-1])
712 713 714 715 716 717 718
    if os.path.isdir(os.path.join(working_copy, '.svn')):
      raise SubversionUnknownBusinessTemplateError, "Could not find '"+\
      bt_name+"' at first level of working copies."
    else:
      raise SubversionNotAWorkingCopyError, \
      "You must do a clean checkout first. It seems that at least one \
      of the paths given in preferences is not a SVN working copy"
719 720

  def _getWorkingPath(self, path):
Christophe Dumez's avatar
Christophe Dumez committed
721 722
    """ Check if the given path is reachable (allowed)
    """
723
    if not path.startswith(self.top_working_path):
724
      raise UnauthorizedAccessToPath, 'Unauthorized access to path %s. It is NOT in your Zope home instance.' % path
725 726
    return path
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
727
  security.declareProtected('Import/Export objects', 'update')
Christophe Dumez's avatar
Christophe Dumez committed
728
  def update(self, business_template):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
729 730
    """Update a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
731
    path = self._getWorkingPath(self.getSubversionPath(business_template))
732 733
    # First remove unversioned in working copy that could conflict
    self.removeAllInList(x['uid'] for x in self.unversionedFiles(path))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
734
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
735 736
    # Revert local changes in working copy first 
    # to import a "pure" BT after update
737 738 739 740
    self.revert(path=path, recurse=True)
    # Update from SVN
    client.update(path)
    # Import in zodb
Christophe Dumez's avatar
Christophe Dumez committed
741
    return self.importBT(business_template)
742
  
Christophe Dumez's avatar
Christophe Dumez committed
743 744
  security.declareProtected('Import/Export objects', 'updatewc')
  def updatewc(self, business_template):
745 746
    """Update a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
747
    path = self._getWorkingPath(self.getSubversionPath(business_template))
748 749 750
    # First do recursive revert to avoid conflicts
    self.revert(path, business_template, True)
    # then remove unversioned in working copy that could conflict
751
    self.removeAllInList(x['uid'] for x in self.unversionedFiles(path))
752 753 754
    client = self._getClient()
    # Update from SVN
    client.update(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
755

756
  security.declareProtected('Import/Export objects', 'switch')
Christophe Dumez's avatar
Christophe Dumez committed
757
  def switch(self, business_template, url):
758 759
    """switch SVN repository for a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
760
    path = self._getWorkingPath(self.getSubversionPath(business_template))
761
    client = self._getClient()
762 763
    if url[-1] == '/' :
      url = url[:-1]
764
    # Update from SVN
765
    client.switch(path=path, url=url)
766
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
767
  security.declareProtected('Import/Export objects', 'add')
768
  # path can be a list or not (relative or absolute)
Christophe Dumez's avatar
Christophe Dumez committed
769
  def add(self, path, business_template=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
770 771
    """Add a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
772
    if business_template is not None:
773
      if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
774 775
        path = [self._getWorkingPath(self.relativeToAbsolute(x, \
        business_template)) for x in path]
776
      else:
Christophe Dumez's avatar
Christophe Dumez committed
777 778
        path = self._getWorkingPath(self.relativeToAbsolute(path, \
        business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
779
    client = self._getClient()
780
    return client.add(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
781

782
  security.declareProtected('Import/Export objects', 'info')
Christophe Dumez's avatar
Christophe Dumez committed
783
  def info(self, business_template):
784 785
    """return info of working copy
    """
Christophe Dumez's avatar
Christophe Dumez committed
786 787
    working_copy = self._getWorkingPath(self\
    .getSubversionPath(business_template))
788 789 790
    client = self._getClient()
    return client.info(working_copy)
  
Christophe Dumez's avatar
Christophe Dumez committed
791
  security.declareProtected('Import/Export objects', 'log')
792
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
793
  def log(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
794 795 796
    """return log of a file or dir
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
797 798
    return client.log(self._getWorkingPath(self\
    .relativeToAbsolute(path, business_template)))
Christophe Dumez's avatar
Christophe Dumez committed
799
  
800
  security.declareProtected('Import/Export objects', 'cleanup')
Christophe Dumez's avatar
Christophe Dumez committed
801
  def cleanup(self, business_template):
802 803
    """remove svn locks in working copy
    """
Christophe Dumez's avatar
Christophe Dumez committed
804 805
    working_copy = self._getWorkingPath(self\
    .getSubversionPath(business_template))
806 807 808
    client = self._getClient()
    return client.cleanup(working_copy)

Yoshinori Okuji's avatar
Yoshinori Okuji committed
809
  security.declareProtected('Import/Export objects', 'remove')
810
  # path can be a list or not (relative or absolute)
Christophe Dumez's avatar
Christophe Dumez committed
811
  def remove(self, path, business_template=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
812 813
    """Remove a file or a directory.
    """
Christophe Dumez's avatar
Christophe Dumez committed
814
    if business_template is not None:
815
      if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
816 817
        path = [self._getWorkingPath(self\
        .relativeToAbsolute(x, business_template)) for x in path]
818
      else:
Christophe Dumez's avatar
Christophe Dumez committed
819 820
        path = self._getWorkingPath(self.relativeToAbsolute(path, \
        business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
821
    client = self._getClient()
822
    return client.remove(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
823 824 825 826 827 828

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

Christophe Dumez's avatar
Christophe Dumez committed
831
  security.declareProtected('Import/Export objects', 'ls')
832
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
833
  def ls(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
834 835 836
    """Display infos about a file.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
837 838
    return client.ls(self._getWorkingPath(self\
    .relativeToAbsolute(path, business_template)))
Christophe Dumez's avatar
Christophe Dumez committed
839

Yoshinori Okuji's avatar
Yoshinori Okuji committed
840
  security.declareProtected('Import/Export objects', 'diff')
841
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
842
  def diff(self, path, business_template, revision1=None, revision2=None):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
843 844 845
    """Make a diff for a file or a directory.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
846 847
    return client.diff(self._getWorkingPath(self.relativeToAbsolute(path, \
    business_template)), revision1, revision2)
848
  
Yoshinori Okuji's avatar
Yoshinori Okuji committed
849
  security.declareProtected('Import/Export objects', 'revert')
850
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
851
  def revert(self, path, business_template=None, recurse=False):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
852 853 854
    """Revert local changes in a file or a directory.
    """
    client = self._getClient()
855
    if not isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
856 857 858 859 860
      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
861
    client.revert(path, recurse)
862 863 864

  security.declareProtected('Import/Export objects', 'revertZODB')
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
865 866
  def revertZODB(self, business_template, added_files=None, \
  other_files=None, recurse=False):
867 868 869 870 871
    """Revert local changes in a file or a directory
       in ZODB and on hard drive
    """
    client = self._getClient()
    object_to_update = {}
Christophe Dumez's avatar
Christophe Dumez committed
872
    # Transform params to list if they are not already lists
873 874 875 876 877
    if not added_files :
      added_files = []
    if not other_files :
      other_files = []
    if not isinstance(added_files, list) :
Christophe Dumez's avatar
Christophe Dumez committed
878
      added_files = [added_files]
879
    if not isinstance(other_files, list) :
Christophe Dumez's avatar
Christophe Dumez committed
880
      other_files = [other_files]
881 882
    
    # Reinstall removed or modified files
Christophe Dumez's avatar
Christophe Dumez committed
883 884
    for path in other_files :
      path_list = self._getWorkingPath(path).split(os.sep)
885 886 887 888
      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
889
          tmp = os.path.splitext(tmp)[0]
890
          object_to_update[tmp] = 'install'
891
    path_added_list = []
892
    # remove added files
Christophe Dumez's avatar
Christophe Dumez committed
893 894
    for path in added_files :
      path_list = self._getWorkingPath(path).split(os.sep)
895 896 897 898
      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
899 900
          tmp = os.path.splitext(tmp)[0]
          path_added_list.append(tmp)
901 902
    ## hack to remove objects
    # Create a temporary bt with objects to delete
Christophe Dumez's avatar
Christophe Dumez committed
903 904
    tmp_bt = getToolByName(business_template, 'portal_templates')\
    .newContent(portal_type="Business Template")
905 906 907 908 909 910 911 912 913
    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
914
    business_template.portal_templates.manage_delObjects(ids=tmp_bt.getId())
915 916
    #revert changes
    added_files.extend(other_files)
Christophe Dumez's avatar
Christophe Dumez committed
917 918
    to_revert = [self.relativeToAbsolute(x, business_template) \
    for x in added_files]
919 920 921
    if len(to_revert) != 0 :
      client.revert(to_revert, recurse)
      # Partially reinstall installed bt
Christophe Dumez's avatar
Christophe Dumez committed
922 923
      installed_bt = business_template.portal_templates\
      .getInstalledBusinessTemplate(business_template.getTitle())
924
      installed_bt.reinstall(object_to_update=object_to_update, force=0)
Christophe Dumez's avatar
Christophe Dumez committed
925 926 927
    
  security.declareProtected('Import/Export objects', 'resolved')
  # path can be absolute or relative
Christophe Dumez's avatar
Christophe Dumez committed
928
  def resolved(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
929 930 931 932
    """remove conflicted status
    """
    client = self._getClient()
    if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
933 934
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
Christophe Dumez's avatar
Christophe Dumez committed
935
    else:
Christophe Dumez's avatar
Christophe Dumez committed
936 937
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
Christophe Dumez's avatar
Christophe Dumez committed
938
    return client.resolved(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
939

Christophe Dumez's avatar
Christophe Dumez committed
940 941 942 943
  def relativeToAbsolute(self, path, business_template):
    """ Return an absolute path given the absolute one
        and the business template
    """
944 945 946 947
    if path[0] == os.sep:
      # already absolute
      return path
    # relative path
Christophe Dumez's avatar
Christophe Dumez committed
948 949 950
    if path.split(os.sep)[0] == business_template.getTitle():
      return os.path.join(self.getSubversionPath(business_template, \
      False), path)
951
    else:
Christophe Dumez's avatar
Christophe Dumez committed
952
      return os.path.join(self.getSubversionPath(business_template), path)
953

Yoshinori Okuji's avatar
Yoshinori Okuji committed
954
  security.declareProtected('Import/Export objects', 'checkin')
955
  # path can be relative or absolute (can be a list of paths too)
Christophe Dumez's avatar
Christophe Dumez committed
956
  def checkin(self, path, business_template, log_message=None, recurse=True):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
957 958
    """Commit local changes.
    """
959
    if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
960 961
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
962
    else:
Christophe Dumez's avatar
Christophe Dumez committed
963 964
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
965
    client = self._getClient()
966
    return client.checkin(path, log_message, recurse)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
967

968
  security.declareProtected('Import/Export objects', 'getLastChangelog')
Christophe Dumez's avatar
Christophe Dumez committed
969
  def getLastChangelog(self, business_template):
970 971
    """Return last changelog of a business template
    """
Christophe Dumez's avatar
Christophe Dumez committed
972
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template))
973
    changelog_path = bt_path + os.sep + 'bt' + os.sep + 'change_log'
Christophe Dumez's avatar
Christophe Dumez committed
974
    changelog = ""
975 976 977 978 979 980 981
    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
982
        changelog += line
983 984 985
    return changelog
    

Yoshinori Okuji's avatar
Yoshinori Okuji committed
986 987 988 989 990
  security.declareProtected('Import/Export objects', 'status')
  def status(self, path, **kw):
    """Get status.
    """
    client = self._getClient()
991
    return client.status(self._getWorkingPath(path), **kw)
992
  
993 994 995 996 997
  security.declareProtected('Import/Export objects', 'unversionedFiles')
  def unversionedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
998
    status_list = client.status(self._getWorkingPath(path), **kw)
999
    unversioned_list = []
Christophe Dumez's avatar
Christophe Dumez committed
1000 1001
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "unversioned":
1002
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
1003
        my_dict['uid'] = status_obj.getPath()
1004 1005 1006
        unversioned_list.append(my_dict)
    return unversioned_list
      
1007 1008 1009 1010 1011
  security.declareProtected('Import/Export objects', 'conflictedFiles')
  def conflictedFiles(self, path, **kw):
    """Return unversioned files
    """
    client = self._getClient()
1012
    status_list = client.status(self._getWorkingPath(path), **kw)
1013
    conflicted_list = []
Christophe Dumez's avatar
Christophe Dumez committed
1014 1015
    for status_obj in status_list:
      if str(status_obj.getTextStatus()) == "conflicted":
1016
        my_dict = {}
Christophe Dumez's avatar
Christophe Dumez committed
1017
        my_dict['uid'] = status_obj.getPath()
1018 1019 1020
        conflicted_list.append(my_dict)
    return conflicted_list

1021
  security.declareProtected('Import/Export objects', 'removeAllInList')
Christophe Dumez's avatar
Christophe Dumez committed
1022
  def removeAllInList(self, path_list):
1023 1024
    """Remove all files and folders in list
    """
Christophe Dumez's avatar
Christophe Dumez committed
1025 1026
    for file_path in path_list:
      removeAll(file_path)
1027
    
Christophe Dumez's avatar
Christophe Dumez committed
1028 1029 1030
  def getModifiedTree(self, business_template, show_unmodified=False) :
    """ Return tree of files returned by svn status
    """
1031
    # Get subversion path without business template name at the end
Christophe Dumez's avatar
Christophe Dumez committed
1032 1033
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template, \
    False))
1034 1035 1036
    if bt_path[-1] != '/':
      bt_path += '/'
    # Business template root directory is the root of the tree
Christophe Dumez's avatar
Christophe Dumez committed
1037 1038
    root = Dir(business_template.getTitle(), "normal")
    something_modified = False
1039
    
1040
    # We browse the files returned by svn status
Christophe Dumez's avatar
Christophe Dumez committed
1041 1042
    for status_obj in self.status(os.path.join(bt_path, \
    business_template.getTitle())) :
1043
      # can be (normal, added, modified, deleted, conflicted, unversioned)
1044 1045
      status = str(status_obj.getTextStatus())
      if (show_unmodified or status != "normal") and status != "unversioned":
Christophe Dumez's avatar
Christophe Dumez committed
1046
        something_modified = True
1047 1048 1049 1050 1051 1052
        # 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
1053
        parent = root
Christophe Dumez's avatar
Christophe Dumez committed
1054
        
1055 1056
        # 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
1057 1058 1059 1060 1061
        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)
1062 1063 1064
        
        # 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
1065
        if os.path.isdir(full_path) :
1066 1067 1068 1069 1070
          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
1071
          else :
1072 1073 1074
            # update msg status
            tmp = parent.getDirFromName(filename)
            tmp.status = str(status)
Christophe Dumez's avatar
Christophe Dumez committed
1075
        else :
1076 1077
          # add new file to the tree
          parent.sub_files.append(File(filename, str(status)))
Christophe Dumez's avatar
Christophe Dumez committed
1078
    return something_modified and root
1079
  
Christophe Dumez's avatar
Christophe Dumez committed
1080 1081 1082 1083 1084 1085 1086 1087 1088
  def extractBT(self, business_template):
    """ 
     Extract business template to hard drive
     and do svn add/del stuff comparing it
     to local working copy
    """
    business_template.build()
    svn_path = self._getWorkingPath(self.getSubversionPath(business_template) \
    + os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
1089
    path = mktemp() + os.sep
1090
    try:
1091 1092
      # XXX: Big hack to make export work as expected.
      get_transaction().commit()
Christophe Dumez's avatar
Christophe Dumez committed
1093
      business_template.export(path=path, local=1)
1094
      # svn del deleted files
Christophe Dumez's avatar
Christophe Dumez committed
1095
      self.deleteOldFiles(svn_path, path)
1096
      # add new files and copy
Christophe Dumez's avatar
Christophe Dumez committed
1097 1098 1099
      self.addNewFiles(svn_path, path)
      self.goToWorkingCopy(business_template)
    except (pysvn.ClientError, NotFound, AttributeError, \
1100
    Error), error:
1101
      # Clean up
1102
      removeAll(path)
1103
      raise error
1104
    # Clean up
Christophe Dumez's avatar
Christophe Dumez committed
1105
    self.activate().removeAllInList([path, ])
1106
    
Christophe Dumez's avatar
Christophe Dumez committed
1107 1108 1109 1110 1111 1112 1113
  def importBT(self, business_template):
    """
     Import business template from local
     working copy
    """
    return business_template.download(self._getWorkingPath(self\
    .getSubversionPath(business_template)))
1114
    
1115 1116
  # Get a list of files and keep only parents
  # Necessary before recursively commit removals
Christophe Dumez's avatar
Christophe Dumez committed
1117 1118 1119 1120 1121 1122 1123 1124
  def cleanChildrenInList(self, path_list):
    """
     Get a list of files and keep only parents
     Necessary before recursively commit removals
    """
    res = path_list
    for file_path in path_list:
      res = [x for x in res if file_path == x or file_path not in x]
1125
    return res
1126

1127 1128
  # return a set with directories present in the directory
  def getSetDirsForDir(self, directory):
1129
    dir_set = set()
Christophe Dumez's avatar
Christophe Dumez committed
1130
    for root, dirs, _ in cacheWalk(directory):
1131 1132 1133 1134 1135
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
      # get Directories
      for name in dirs:
1136
        i = root.replace(directory, '').count(os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
1137 1138
        path = os.path.join(root, name)
        dir_set.add((i, path.replace(directory,'')))
1139 1140 1141 1142 1143
    return dir_set
      
  # return a set with files present in the directory
  def getSetFilesForDir(self, directory):
    dir_set = set()
1144
    for root, dirs, files in cacheWalk(directory):
1145 1146 1147
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
1148
      # get Files
1149 1150
      for name in files:
        i = root.replace(directory, '').count(os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
1151 1152
        path = os.path.join(root, name)
        dir_set.add((i, path.replace(directory,'')))
1153
    return dir_set
1154
  
1155
  # return files present in new_dir but not in old_dir
1156 1157
  # return a set of relative paths
  def getNewFiles(self, old_dir, new_dir):
1158 1159 1160 1161
    if old_dir[-1] != os.sep:
      old_dir += os.sep
    if new_dir[-1] != os.sep:
      new_dir += os.sep
1162 1163
    old_set = self.getSetFilesForDir(old_dir)
    new_set = self.getSetFilesForDir(new_dir)
1164 1165
    return new_set.difference(old_set)

1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176
  # 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
1177 1178 1179
  def deleteOldFiles(self, old_dir, new_dir):
    """ svn del files that have been removed in new dir
    """
1180
    # detect removed files
1181
    files_set = self.getNewFiles(new_dir, old_dir)
1182 1183
    # detect removed directories
    dirs_set = self.getNewDirs(new_dir, old_dir)
1184
    # svn del
Christophe Dumez's avatar
Christophe Dumez committed
1185 1186 1187 1188 1189 1190
    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])
1191
  
Christophe Dumez's avatar
Christophe Dumez committed
1192 1193 1194
  def addNewFiles(self, old_dir, new_dir):
    """ copy files and add new files
    """
1195
    # detect created files
1196
    files_set = self.getNewFiles(old_dir, new_dir)
1197 1198
    # detect created directories
    dirs_set = self.getNewDirs(old_dir, new_dir)
1199
    # Copy files
1200
    copytree(new_dir, old_dir)
1201
    # svn add
Christophe Dumez's avatar
Christophe Dumez committed
1202 1203 1204 1205 1206 1207
    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])
1208
  
Christophe Dumez's avatar
Christophe Dumez committed
1209 1210 1211
  def treeToXML(self, item, business_template) :
    """ Convert tree in memory to XML
    """
1212
    output = '<?xml version="1.0" encoding="UTF-8"?>'+ os.linesep
1213
    output += "<tree id='0'>" + os.linesep
Christophe Dumez's avatar
Christophe Dumez committed
1214
    output = self._treeToXML(item, output, business_template.getTitle(), True)
1215
    output += '</tree>' + os.linesep
1216
    return output
1217
  
1218
  def _treeToXML(self, item, output, relative_path, first) :
Christophe Dumez's avatar
Christophe Dumez committed
1219 1220 1221 1222
    """
     Private function to convert recursively tree 
     in memory to XML
    """
1223
    # Choosing a color coresponding to the status
1224 1225 1226 1227 1228 1229 1230 1231 1232
    status = item.status
    if status == 'added' :
      color = 'green'
    elif status == 'modified' or  status == 'replaced' :
      color = 'orange'
    elif status == 'deleted' :
      color = 'red'
    elif status == 'conflicted' :
      color = 'grey'
Christophe Dumez's avatar
Christophe Dumez committed
1233
    else :
1234
      color = 'black'
1235
    if isinstance(item, Dir) :
Christophe Dumez's avatar
Christophe Dumez committed
1236
      if first :
1237
        output += '<item open="1" text="%s" id="%s" aCol="%s" '\
Christophe Dumez's avatar
Christophe Dumez committed
1238
        'im0="folder.png" im1="folder_open.png" '\
1239
        'im2="folder.png">'%(item.name, relative_path, color) + os.linesep
1240
        first = False
Christophe Dumez's avatar
Christophe Dumez committed
1241
      else :
1242
        output += '<item text="%s" id="%s" aCol="%s" im0="folder.png" ' \
1243 1244 1245
        '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
1246 1247
        output = self._treeToXML(item.getObjectFromName(it.name), output, \
        os.path.join(relative_path,it.name),first)
1248
      output += '</item>' + os.linesep
1249
    else :
1250
      output += '<item text="%s" id="%s" aCol="%s" im0="document.png"/>'\
1251
                %(item.name, relative_path, color) + os.linesep
1252
    return output
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1253 1254
    
InitializeClass(SubversionTool)