SubversionTool.py 43.8 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
50
from time import ctime
Aurel's avatar
Aurel committed
51

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

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

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

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

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

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

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

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

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

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

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

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

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

Yoshinori Okuji's avatar
Yoshinori Okuji committed
478 479

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

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

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

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

  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)
681 682
  
  security.declareProtected('Import/Export objects', 'getSubversionPath')
Christophe Dumez's avatar
Christophe Dumez committed
683 684 685 686 687 688 689 690 691 692 693
  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()
694
    if not wc_list:
Christophe Dumez's avatar
Christophe Dumez committed
695 696
      wc_list = self.getPortalObject().portal_preferences.\
      default_site_preference.getPreferredSubversionWorkingCopyList()
697
      if not wc_list:
Christophe Dumez's avatar
Christophe Dumez committed
698 699
        raise SubversionPreferencesError, \
        'Please set at least one Subversion Working Copy in preferences first.'
700
    if len(wc_list) == 0 :
Christophe Dumez's avatar
Christophe Dumez committed
701 702 703 704 705 706 707
      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)
708 709 710 711 712
        if os.path.isdir(wc_path):
          if with_name:
            return wc_path
          else:
            return os.sep.join(wc_path.split(os.sep)[:-1])
713 714 715 716 717 718 719
    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"
720 721

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

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

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

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

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

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

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

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

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

Yoshinori Okuji's avatar
Yoshinori Okuji committed
959
  security.declareProtected('Import/Export objects', 'checkin')
960
  # path can be relative or absolute (can be a list of paths too)
Christophe Dumez's avatar
Christophe Dumez committed
961
  def checkin(self, path, business_template, log_message=None, recurse=True):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
962 963
    """Commit local changes.
    """
964
    if isinstance(path, list) :
Christophe Dumez's avatar
Christophe Dumez committed
965 966
      path = [self._getWorkingPath(self.relativeToAbsolute(x, \
      business_template)) for x in path]
967
    else:
Christophe Dumez's avatar
Christophe Dumez committed
968 969
      path = self._getWorkingPath(self.relativeToAbsolute(path, \
      business_template))
970
    client = self._getClient()
971 972 973
    # Pysvn wants unicode objects
    if isinstance(log_message, str):
      log_message = log_message.decode('utf8')
974
    return client.checkin(path, log_message, recurse)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
975

976
  security.declareProtected('Import/Export objects', 'getLastChangelog')
Christophe Dumez's avatar
Christophe Dumez committed
977
  def getLastChangelog(self, business_template):
978 979
    """Return last changelog of a business template
    """
Christophe Dumez's avatar
Christophe Dumez committed
980
    bt_path = self._getWorkingPath(self.getSubversionPath(business_template))
981
    changelog_path = bt_path + os.sep + 'bt' + os.sep + 'change_log'
Christophe Dumez's avatar
Christophe Dumez committed
982
    changelog = ""
983 984 985 986 987 988 989
    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
990
        changelog += line
991 992 993
    return changelog
    

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

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

1137 1138
  # return a set with directories present in the directory
  def getSetDirsForDir(self, directory):
1139
    dir_set = set()
Christophe Dumez's avatar
Christophe Dumez committed
1140
    for root, dirs, _ in cacheWalk(directory):
1141 1142 1143 1144 1145
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
      # get Directories
      for name in dirs:
1146
        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 1150 1151 1152 1153
    return dir_set
      
  # return a set with files present in the directory
  def getSetFilesForDir(self, directory):
    dir_set = set()
1154
    for root, dirs, files in cacheWalk(directory):
1155 1156 1157
      # don't visit SVN directories
      if '.svn' in dirs:
        dirs.remove('.svn')
1158
      # get Files
1159 1160
      for name in files:
        i = root.replace(directory, '').count(os.sep)
Christophe Dumez's avatar
Christophe Dumez committed
1161 1162
        path = os.path.join(root, name)
        dir_set.add((i, path.replace(directory,'')))
1163
    return dir_set
1164
  
1165
  # return files present in new_dir but not in old_dir
1166 1167
  # return a set of relative paths
  def getNewFiles(self, old_dir, new_dir):
1168 1169 1170 1171
    if old_dir[-1] != os.sep:
      old_dir += os.sep
    if new_dir[-1] != os.sep:
      new_dir += os.sep
1172 1173
    old_set = self.getSetFilesForDir(old_dir)
    new_set = self.getSetFilesForDir(new_dir)
1174 1175
    return new_set.difference(old_set)

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