SubversionTool.py 43.5 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/>''')
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()
736 737 738
    # Revert local changes in working copy first
    # to import a "pure" BT after update
    self.revert(path=path, recurse=True)
739
    # removed unversioned files due to former added files that were reverted
740
    self.removeAllInList(x['uid'] for x in self.unversionedFiles(path))
741 742 743
    # Update from SVN
    client.update(path)
    # Import in zodb
Christophe Dumez's avatar
Christophe Dumez committed
744
    return self.importBT(business_template)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
745

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

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

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

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

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

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

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

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

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

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

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

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

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

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