SubversionTool.py 42.7 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
Christophe Dumez's avatar
Christophe Dumez committed
38
import os, re, commands, time, exceptions, pysvn
Yoshinori Okuji's avatar
Yoshinori Okuji committed
39 40 41 42
from DateTime import DateTime
from cPickle import dumps, loads
from App.config import getConfiguration
from zExceptions import Unauthorized
Christophe Dumez's avatar
Christophe Dumez committed
43 44
from OFS.Image import manage_addFile
from cStringIO import StringIO
45
from tempfile import mktemp
46
from Products.CMFCore.utils import getToolByName
Christophe Dumez's avatar
Christophe Dumez committed
47 48
from Products.ERP5.Document.BusinessTemplate import removeAll
from Products.ERP5.Document.BusinessTemplate import TemplateConditionError
49
from xml.sax.saxutils import escape
50
from dircache import listdir
51
from OFS.Traversable import NotFound
52
from Products.ERP5Type.copyTree import copytree
53
from Products.ERP5Type.cacheWalk import cacheWalk
Aurel's avatar
Aurel committed
54 55 56 57

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

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

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

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

class Error(exceptions.EnvironmentError):
Christophe Dumez's avatar
Christophe Dumez committed
121 122 123
  """ Simple Exception
  """
  pass
124

125 126 127 128 129 130 131 132 133
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
134 135 136 137 138

class SubversionNotAWorkingCopyError(Exception):
  """The base exception class when business template is unknown.
  """
  pass
139 140

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

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

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

class CodeBlock:
Christophe Dumez's avatar
Christophe Dumez committed
276 277 278 279 280 281 282 283 284 285 286
  """
   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)
  """
287

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

Yoshinori Okuji's avatar
Yoshinori Okuji committed
476 477

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

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

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

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

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

  def _getWorkingPath(self, path):
Christophe Dumez's avatar
Christophe Dumez committed
719 720
    """ Check if the given path is reachable (allowed)
    """
721 722 723 724
    if not path.startswith(self.top_working_path):
      raise Unauthorized, 'unauthorized access to path %s' % path
    return path
    
Yoshinori Okuji's avatar
Yoshinori Okuji committed
725
  security.declareProtected('Import/Export objects', 'update')
Christophe Dumez's avatar
Christophe Dumez committed
726
  def update(self, business_template):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
727 728
    """Update a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
729
    path = self._getWorkingPath(self.getSubversionPath(business_template))
Yoshinori Okuji's avatar
Yoshinori Okuji committed
730
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
731 732
    # Revert local changes in working copy first 
    # to import a "pure" BT after update
733 734 735 736
    self.revert(path=path, recurse=True)
    # Update from SVN
    client.update(path)
    # Import in zodb
Christophe Dumez's avatar
Christophe Dumez committed
737
    return self.importBT(business_template)
738
  
Christophe Dumez's avatar
Christophe Dumez committed
739 740
  security.declareProtected('Import/Export objects', 'updatewc')
  def updatewc(self, business_template):
741 742
    """Update a working copy.
    """
Christophe Dumez's avatar
Christophe Dumez committed
743
    path = self._getWorkingPath(self.getSubversionPath(business_template))
744 745 746
    client = self._getClient()
    # Update from SVN
    client.update(path)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
747

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

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

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

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

Christophe Dumez's avatar
Christophe Dumez committed
823
  security.declareProtected('Import/Export objects', 'ls')
824
  # path can be relative or absolute
Christophe Dumez's avatar
Christophe Dumez committed
825
  def ls(self, path, business_template):
Christophe Dumez's avatar
Christophe Dumez committed
826 827 828
    """Display infos about a file.
    """
    client = self._getClient()
Christophe Dumez's avatar
Christophe Dumez committed
829 830
    return client.ls(self._getWorkingPath(self\
    .relativeToAbsolute(path, business_template)))
Christophe Dumez's avatar
Christophe Dumez committed
831

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

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

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

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

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

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

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

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

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