SubversionTool.py 42.9 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
    # First remove unversioned in working copy that could conflict
    self.removeAllInList(x['uid'] for x in self.unversionedFiles(path))
750 751 752
    client = self._getClient()
    # Update from SVN
    client.update(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
753

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

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

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

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

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

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

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

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

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

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

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

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

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

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