~ubuntu-branches/ubuntu/precise/trac/precise

« back to all changes in this revision

Viewing changes to trac/versioncontrol/svn_fs.py

  • Committer: Bazaar Package Importer
  • Author(s): Luis Matos
  • Date: 2008-07-13 23:46:20 UTC
  • mfrom: (1.1.13 upstream)
  • Revision ID: james.westby@ubuntu.com-20080713234620-13ynpdpkbaymfg1z
Tags: 0.11-2
* Re-added python-setup-tools to build dependences. Closes: #490320 #468705
* New upstream release Closes: 489727
* Added sugestion for other vcs support available: git bazaar mercurial 
* Added spamfilter plugin to sugests
* Moved packaging from python-support to python-central
* Added an entry to the NEWS about the cgi Closes: #490275
* Updated 10_remove_trac_suffix_from_title patch to be used in 0.11

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- coding: utf-8 -*-
2
2
#
3
 
# Copyright (C) 2005-2006 Edgewall Software
 
3
# Copyright (C) 2005-2008 Edgewall Software
4
4
# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
5
 
# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
 
5
# Copyright (C) 2005-2007 Christian Boos <cboos@neuf.fr>
6
6
# All rights reserved.
7
7
#
8
8
# This software is licensed as described in the file COPYING, which
24
24
  All paths manipulated by Trac are `unicode` objects.
25
25
 
26
26
  Therefore:
27
 
   * before being handed out to SVN, the Trac paths have to be encoded to UTF-8,
28
 
     using `_to_svn()`
29
 
   * before being handed out to Trac, a SVN path has to be decoded from UTF-8,
30
 
     using `_from_svn()`
 
27
   * before being handed out to SVN, the Trac paths have to be encoded to
 
28
     UTF-8, using `_to_svn()`
 
29
   * before being handed out to Trac, a SVN path has to be decoded from
 
30
     UTF-8, using `_from_svn()`
31
31
 
32
32
  Warning: `SubversionNode.get_content` returns an object from which one
33
33
           can read a stream of bytes.
43
43
import time
44
44
import weakref
45
45
import posixpath
46
 
 
 
46
from datetime import datetime
 
47
 
 
48
from genshi.builder import tag
 
49
 
 
50
from trac.config import ListOption
47
51
from trac.core import *
48
52
from trac.versioncontrol import Changeset, Node, Repository, \
49
53
                                IRepositoryConnector, \
50
54
                                NoSuchChangeset, NoSuchNode
51
55
from trac.versioncontrol.cache import CachedRepository
52
56
from trac.versioncontrol.svn_authz import SubversionAuthorizer
 
57
from trac.versioncontrol.web_ui.browser import IPropertyRenderer
 
58
from trac.util import sorted, embedded_numbers, reversed
53
59
from trac.util.text import to_unicode
54
 
 
55
 
try:
 
60
from trac.util.translation import _
 
61
from trac.util.datefmt import utc
 
62
 
 
63
 
 
64
application_pool = None
 
65
 
 
66
 
 
67
def _import_svn():
 
68
    global fs, repos, core, delta, _kindmap
56
69
    from svn import fs, repos, core, delta
57
 
    has_subversion = True
58
 
except ImportError:
59
 
    has_subversion = False
60
 
    class dummy_svn(object):
61
 
        svn_node_dir = 1
62
 
        svn_node_file = 2
63
 
        def apr_pool_destroy(): pass
64
 
        def apr_terminate(): pass
65
 
        def apr_pool_clear(): pass
66
 
        Editor = object
67
 
    delta = core = dummy_svn()
68
 
    
69
 
 
70
 
_kindmap = {core.svn_node_dir: Node.DIRECTORY,
71
 
            core.svn_node_file: Node.FILE}
72
 
 
73
 
 
74
 
application_pool = None
 
70
    _kindmap = {core.svn_node_dir: Node.DIRECTORY,
 
71
                core.svn_node_file: Node.FILE}
 
72
    # Protect svn.core methods from GC
 
73
    Pool.apr_pool_clear = staticmethod(core.apr_pool_clear)
 
74
    Pool.apr_terminate = staticmethod(core.apr_terminate)
 
75
    Pool.apr_pool_destroy = staticmethod(core.apr_pool_destroy)
75
76
 
76
77
def _to_svn(*args):
77
78
    """Expect a list of `unicode` path components.
78
 
    Returns an UTF-8 encoded string suitable for the Subversion python bindings.
 
79
    
 
80
    Returns an UTF-8 encoded string suitable for the Subversion python bindings
 
81
    (the returned path never starts with a leading "/")
79
82
    """
80
 
    return '/'.join([path.strip('/') for path in args]).encode('utf-8')
81
 
    
 
83
    return '/'.join([p for p in [p.strip('/') for p in args] if p]) \
 
84
           .encode('utf-8')
 
85
 
82
86
def _from_svn(path):
83
 
    """Expect an UTF-8 encoded string and transform it to an `unicode` object"""
84
 
    return path and path.decode('utf-8')
 
87
    """Expect an UTF-8 encoded string and transform it to an `unicode` object
 
88
    But Subversion repositories built from conversion utilities can have
 
89
    non-UTF-8, so we have to handle it.
 
90
    """
 
91
    return path and to_unicode(path, 'utf-8')
85
92
    
86
93
def _normalize_path(path):
87
94
    """Remove leading "/", except for the root."""
108
115
    scope = scope.strip('/')
109
116
    return (fullpath + '/').startswith(scope + '/')
110
117
 
 
118
# svn_opt_revision_t helpers
 
119
 
 
120
def _svn_rev(num):
 
121
    value = core.svn_opt_revision_value_t()
 
122
    value.number = num
 
123
    revision = core.svn_opt_revision_t()
 
124
    revision.kind = core.svn_opt_revision_number
 
125
    revision.value = value
 
126
    return revision
 
127
 
 
128
def _svn_head():
 
129
    revision = core.svn_opt_revision_t()
 
130
    revision.kind = core.svn_opt_revision_head
 
131
    return revision
 
132
 
 
133
# apr_pool_t helpers
111
134
 
112
135
def _mark_weakpool_invalid(weakpool):
113
136
    if weakpool():
117
140
class Pool(object):
118
141
    """A Pythonic memory pool object"""
119
142
 
120
 
    # Protect svn.core methods from GC
121
 
    apr_pool_destroy = staticmethod(core.apr_pool_destroy)
122
 
    apr_terminate = staticmethod(core.apr_terminate)
123
 
    apr_pool_clear = staticmethod(core.apr_pool_clear)
124
 
    
125
143
    def __init__(self, parent_pool=None):
126
144
        """Create a new memory pool"""
127
145
 
210
228
                del self._weakref
211
229
 
212
230
 
213
 
# Initialize application-level pool
214
 
if has_subversion:
215
 
    Pool()
216
 
 
217
 
 
218
231
class SubversionConnector(Component):
219
232
 
220
233
    implements(IRepositoryConnector)
221
234
 
 
235
    branches = ListOption('svn', 'branches', 'trunk,branches/*', doc=
 
236
        """List of paths categorized as ''branches''.
 
237
        If a path ends with '*', then all the directory entries found
 
238
        below that path will be included.
 
239
        """)
 
240
 
 
241
    tags = ListOption('svn', 'tags', 'tags/*', doc=
 
242
        """List of paths categorized as ''tags''.
 
243
        If a path ends with '*', then all the directory entries found
 
244
        below that path will be included.
 
245
        """)
 
246
 
 
247
    def __init__(self):
 
248
        self._version = None
 
249
        
 
250
        try:
 
251
            _import_svn()
 
252
            self.log.debug('Subversion bindings imported')
 
253
        except ImportError:
 
254
            self.log.info('Failed to load Subversion bindings', exc_info=True)
 
255
            self.has_subversion = False
 
256
        else:
 
257
            self.has_subversion = True
 
258
            Pool()
 
259
 
222
260
    def get_supported_types(self):
223
 
        global has_subversion
224
 
        if has_subversion:
 
261
        if self.has_subversion:
 
262
            yield ("direct-svnfs", 4)
225
263
            yield ("svnfs", 4)
226
264
            yield ("svn", 2)
227
265
 
228
266
    def get_repository(self, type, dir, authname):
229
267
        """Return a `SubversionRepository`.
230
268
 
231
 
        The repository is generally wrapped in a `CachedRepository`,
232
 
        unless `direct-svn-fs` is the specified type.
 
269
        The repository is wrapped in a `CachedRepository`, unless `type` is
 
270
        'direct-svnfs'.
233
271
        """
234
 
        repos = SubversionRepository(dir, None, self.log)
235
 
        crepos = CachedRepository(self.env.get_db_cnx(), repos, None, self.log)
 
272
        if not self._version:
 
273
            self._version = self._get_version()
 
274
            self.env.systeminfo.append(('Subversion', self._version))
 
275
        fs_repos = SubversionRepository(dir, None, self.log,
 
276
                                        {'tags': self.tags,
 
277
                                         'branches': self.branches})
 
278
        if type == 'direct-svnfs':
 
279
            repos = fs_repos
 
280
        else:
 
281
            repos = CachedRepository(self.env.get_db_cnx(), fs_repos, None,
 
282
                                     self.log)
 
283
            repos.has_linear_changesets = True
236
284
        if authname:
237
 
            authz = SubversionAuthorizer(self.env, crepos, authname)
238
 
            repos.authz = crepos.authz = authz
239
 
        return crepos
240
 
            
 
285
            authz = SubversionAuthorizer(self.env, weakref.proxy(repos),
 
286
                                         authname)
 
287
            repos.authz = fs_repos.authz = authz
 
288
        return repos
 
289
 
 
290
    def _get_version(self):
 
291
        version = (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO)
 
292
        version_string = '%d.%d.%d' % version + core.SVN_VER_TAG
 
293
        if version[0] < 1:
 
294
            raise TracError(_("Subversion >= 1.0 required: Found %(version)s",
 
295
                              version=version_string))
 
296
        return version_string
 
297
 
 
298
 
 
299
class SubversionPropertyRenderer(Component):
 
300
    implements(IPropertyRenderer)
 
301
 
 
302
    def __init__(self):
 
303
        self._externals_map = {}
 
304
 
 
305
    # IPropertyRenderer methods
 
306
 
 
307
    def match_property(self, name, mode):
 
308
        return name in ('svn:externals', 'svn:needs-lock') and 4 or 0
 
309
    
 
310
    def render_property(self, name, mode, context, props):
 
311
        if name == 'svn:externals':
 
312
            return self._render_externals(props[name])
 
313
        elif name == 'svn:needs-lock':
 
314
            return self._render_needslock(context)
 
315
 
 
316
    def _render_externals(self, prop):
 
317
        if not self._externals_map:
 
318
            for dummykey, value in self.config.options('svn:externals'):
 
319
                value = value.split()
 
320
                if len(value) != 2:
 
321
                    self.env.warn("svn:externals entry %s doesn't contain "
 
322
                            "a space-separated key value pair, skipping.", 
 
323
                            label)
 
324
                    continue
 
325
                key, value = value
 
326
                self._externals_map[key] = value.replace('%', '%%') \
 
327
                                           .replace('$path', '%(path)s') \
 
328
                                           .replace('$rev', '%(rev)s')
 
329
        externals = []
 
330
        for external in prop.splitlines():
 
331
            elements = external.split()
 
332
            if not elements:
 
333
                continue
 
334
            localpath, rev, url = elements[0], '', elements[-1]
 
335
            if localpath.startswith('#'):
 
336
                externals.append((external, None, None, None, None))
 
337
                continue
 
338
            if len(elements) == 3:
 
339
                rev = elements[1]
 
340
                rev = rev.replace('-r', '')
 
341
            # retrieve a matching entry in the externals map
 
342
            prefix = []
 
343
            base_url = url
 
344
            while base_url:
 
345
                if base_url in self._externals_map or base_url==u'/':
 
346
                    break
 
347
                base_url, pref = posixpath.split(base_url)
 
348
                prefix.append(pref)
 
349
            href = self._externals_map.get(base_url)
 
350
            revstr = rev and ' at revision '+rev or ''
 
351
            if not href and (url.startswith('http://') or 
 
352
                             url.startswith('https://')):
 
353
                href = url.replace('%', '%%')
 
354
            if href:
 
355
                remotepath = posixpath.join(*reversed(prefix))
 
356
                externals.append((localpath, revstr, base_url, remotepath,
 
357
                                  href % {'path': remotepath, 'rev': rev}))
 
358
            else:
 
359
                externals.append((localpath, revstr, url, None, None))
 
360
        externals_data = []
 
361
        for localpath, rev, url, remotepath, href in externals:
 
362
            label = localpath
 
363
            if url is None:
 
364
                title = ''
 
365
            elif href:
 
366
                if url:
 
367
                    url = ' in ' + url
 
368
                label += rev + url
 
369
                title = ''.join((remotepath, rev, url))
 
370
            else:
 
371
                title = _('No svn:externals configured in trac.ini')
 
372
            externals_data.append((label, href, title))
 
373
        return tag.ul([tag.li(tag.a(label, href=href, title=title))
 
374
                       for label, href, title in externals_data])
 
375
 
 
376
    def _render_needslock(self, context):
 
377
        return tag.img(src=context.href.chrome('common/lock-locked.png'),
 
378
                       alt="needs lock", title="needs lock")
241
379
 
242
380
 
243
381
class SubversionRepository(Repository):
244
 
    """
245
 
    Repository implementation based on the svn.fs API.
246
 
    """
 
382
    """Repository implementation based on the svn.fs API."""
247
383
 
248
 
    def __init__(self, path, authz, log):
249
 
        self.path = path # might be needed by __del__()/close()
 
384
    def __init__(self, path, authz, log, options={}):
250
385
        self.log = log
251
 
        if core.SVN_VER_MAJOR < 1:
252
 
            raise TracError("Subversion >= 1.0 required: Found %d.%d.%d" % \
253
 
                            (core.SVN_VER_MAJOR,
254
 
                             core.SVN_VER_MINOR,
255
 
                             core.SVN_VER_MICRO))
 
386
        self.options = options
256
387
        self.pool = Pool()
257
388
        
258
389
        # Remove any trailing slash or else subversion might abort
261
392
        path = os.path.normpath(path).replace('\\', '/')
262
393
        self.path = repos.svn_repos_find_root_path(path, self.pool())
263
394
        if self.path is None:
264
 
            raise TracError("%s does not appear to be a Subversion repository." \
265
 
                            % path)
 
395
            raise TracError(_("%(path)s does not appear to be a Subversion "
 
396
                              "repository.", path=path))
266
397
 
267
398
        self.repos = repos.svn_repos_open(self.path, self.pool())
268
399
        self.fs_ptr = repos.svn_repos_fs(self.repos)
302
433
        return _normalize_path(path)
303
434
 
304
435
    def normalize_rev(self, rev):
305
 
        try:
306
 
            rev =  int(rev)
307
 
        except (ValueError, TypeError):
308
 
            rev = None
309
 
        if rev is None:
310
 
            rev = self.youngest_rev
311
 
        elif rev > self.youngest_rev:
 
436
        if rev is None or isinstance(rev, basestring) and \
 
437
               rev.lower() in ('', 'head', 'latest', 'youngest'):
 
438
            return self.youngest_rev
 
439
        else:
 
440
            try:
 
441
                rev = int(rev)
 
442
                if rev <= self.youngest_rev:
 
443
                    return rev
 
444
            except (ValueError, TypeError):
 
445
                pass
312
446
            raise NoSuchChangeset(rev)
313
 
        return rev
314
447
 
315
448
    def close(self):
316
 
        self.repos = None
317
 
        self.fs_ptr = None
318
 
        self.pool = None
 
449
        self.repos = self.fs_ptr = self.pool = None
 
450
 
 
451
    def _get_tags_or_branches(self, paths):
 
452
        """Retrieve known branches or tags."""
 
453
        for path in self.options.get(paths, []):
 
454
            if path.endswith('*'):
 
455
                folder = posixpath.dirname(path)
 
456
                try:
 
457
                    entries = [n for n in self.get_node(folder).get_entries()]
 
458
                    for node in sorted(entries, key=lambda n: 
 
459
                                       embedded_numbers(n.path.lower())):
 
460
                        if node.kind == Node.DIRECTORY:
 
461
                            yield node
 
462
                except: # no right (TODO: should use a specific Exception here)
 
463
                    pass
 
464
            else:
 
465
                try:
 
466
                    yield self.get_node(path)
 
467
                except: # no right
 
468
                    pass
 
469
 
 
470
    def get_quickjump_entries(self, rev):
 
471
        """Retrieve known branches, as (name, id) pairs.
 
472
        
 
473
        Purposedly ignores `rev` and always takes the last revision.
 
474
        """
 
475
        for n in self._get_tags_or_branches('branches'):
 
476
            yield 'branches', n.path, n.path, None
 
477
        for n in self._get_tags_or_branches('tags'):
 
478
            yield 'tags', n.path, n.created_path, n.created_rev
319
479
 
320
480
    def get_changeset(self, rev):
321
481
        rev = self.normalize_rev(rev)
324
484
 
325
485
    def get_node(self, path, rev=None):
326
486
        path = path or ''
327
 
        self.authz.assert_permission(posixpath.join(self.scope, path))
 
487
        self.authz.assert_permission(posixpath.join(self.scope,
 
488
                                                    path.strip('/')))
328
489
        if path and path[-1] == '/':
329
490
            path = path[:-1]
330
491
 
331
 
        rev = self.normalize_rev(rev)
 
492
        rev = self.normalize_rev(rev) or self.youngest_rev
332
493
 
333
494
        return SubversionNode(path, rev, self, self.pool)
334
495
 
342
503
        if start < end:
343
504
            start, end = end, start
344
505
        root = fs.revision_root(self.fs_ptr, start, pool())
345
 
        history_ptr = fs.node_history(root, svn_path, pool())
 
506
        tmp1 = Pool(pool)
 
507
        tmp2 = Pool(pool)
 
508
        history_ptr = fs.node_history(root, svn_path, tmp1())
346
509
        cross_copies = 1
347
510
        while history_ptr:
348
 
            history_ptr = fs.history_prev(history_ptr, cross_copies, pool())
 
511
            history_ptr = fs.history_prev(history_ptr, cross_copies, tmp2())
 
512
            tmp1.clear()
 
513
            tmp1, tmp2 = tmp2, tmp1
349
514
            if history_ptr:
350
 
                path, rev = fs.history_location(history_ptr, pool())
 
515
                path, rev = fs.history_location(history_ptr, tmp2())
 
516
                tmp2.clear()
351
517
                if rev < end:
352
518
                    break
353
519
                path = _from_svn(path)
354
520
                if not self.authz.has_permission(path):
355
521
                    break
356
522
                yield path, rev
357
 
 
 
523
        del tmp1
 
524
        del tmp2
 
525
    
358
526
    def _previous_rev(self, rev, path='', pool=None):
359
527
        if rev > 1: # don't use oldest here, as it's too expensive
360
528
            try:
370
538
    def get_oldest_rev(self):
371
539
        if self.oldest is None:
372
540
            self.oldest = 1
373
 
            if self.scope != '/':
374
 
                self.oldest = self.next_rev(0, find_initial_rev=True)
 
541
            # trying to figure out the oldest rev for scoped repository
 
542
            # is too expensive and uncovers a big memory leak (#5213)
 
543
            # if self.scope != '/':
 
544
            #    self.oldest = self.next_rev(0, find_initial_rev=True)
375
545
        return self.oldest
376
546
 
377
547
    def get_youngest_rev(self):
412
582
    def get_youngest_rev_in_cache(self, db):
413
583
        """Get the latest stored revision by sorting the revision strings
414
584
        numerically
 
585
 
 
586
        (deprecated, only used for transparent migration to the new caching
 
587
        scheme).
415
588
        """
416
589
        cursor = db.cursor()
417
590
        cursor.execute("SELECT rev FROM revision "
473
646
        if self.has_node(new_path, new_rev):
474
647
            new_node = self.get_node(new_path, new_rev)
475
648
        else:
476
 
            raise NoSuchNode(new_path, new_rev, 'The Target for Diff is invalid')
 
649
            raise NoSuchNode(new_path, new_rev,
 
650
                             'The Target for Diff is invalid')
477
651
        if new_node.kind != old_node.kind:
478
 
            raise TracError('Diff mismatch: Base is a %s (%s in revision %s) '
479
 
                            'and Target is a %s (%s in revision %s).' \
480
 
                            % (old_node.kind, old_path, old_rev,
481
 
                               new_node.kind, new_path, new_rev))
 
652
            raise TracError(_('Diff mismatch: Base is a %(oldnode)s '
 
653
                              '(%(oldpath)s in revision %(oldrev)s) and '
 
654
                              'Target is a %(newnode)s (%(newpath)s in '
 
655
                              'revision %(newrev)s).', oldnode=old_node.kind,
 
656
                              oldpath=old_path, oldrev=old_rev,
 
657
                              newnode=new_node.kind, newpath=new_path,
 
658
                              newrev=new_rev))
482
659
        subpool = Pool(self.pool)
483
660
        if new_node.isdir:
484
661
            editor = DiffChangeEditor()
524
701
 
525
702
class SubversionNode(Node):
526
703
 
527
 
    def __init__(self, path, rev, repos, pool=None):
 
704
    def __init__(self, path, rev, repos, pool=None, parent=None):
528
705
        self.repos = repos
529
706
        self.fs_ptr = repos.fs_ptr
530
707
        self.authz = repos.authz
532
709
        self._scoped_svn_path = _to_svn(self.scope, path)
533
710
        self.pool = Pool(pool)
534
711
        self._requested_rev = rev
 
712
        pool = self.pool()
535
713
 
536
 
        self.root = fs.revision_root(self.fs_ptr, rev, self.pool())
537
 
        node_type = fs.check_path(self.root, self._scoped_svn_path,
538
 
                                  self.pool())
 
714
        if parent and parent._requested_rev == self._requested_rev:
 
715
            self.root = parent.root
 
716
        else:
 
717
            self.root = fs.revision_root(self.fs_ptr, rev, self.pool())
 
718
        node_type = fs.check_path(self.root, self._scoped_svn_path, pool)
539
719
        if not node_type in _kindmap:
540
720
            raise NoSuchNode(path, rev)
541
 
        cr = fs.node_created_rev(self.root, self._scoped_svn_path, self.pool())
542
 
        cp = fs.node_created_path(self.root, self._scoped_svn_path, self.pool())
 
721
        cr = fs.node_created_rev(self.root, self._scoped_svn_path, pool)
 
722
        cp = fs.node_created_path(self.root, self._scoped_svn_path, pool)
543
723
        # Note: `cp` differs from `path` if the last change was a copy,
544
724
        #        In that case, `path` doesn't even exist at `cr`.
545
725
        #        The only guarantees are:
573
753
        entries = fs.dir_entries(self.root, self._scoped_svn_path, pool())
574
754
        for item in entries.keys():
575
755
            path = posixpath.join(self.path, _from_svn(item))
576
 
            if not self.authz.has_permission(path):
 
756
            if not self.authz.has_permission(posixpath.join(self.scope,
 
757
                                                            path.strip('/'))):
577
758
                continue
578
759
            yield SubversionNode(path, self._requested_rev, self.repos,
579
 
                                 self.pool)
 
760
                                 self.pool, self)
580
761
 
581
762
    def get_history(self, limit=None):
582
763
        newer = None # 'newer' is the previously seen history tuple
602
783
        if newer:
603
784
            yield newer
604
785
 
 
786
    def get_annotations(self):
 
787
        annotations = []
 
788
        if self.isfile:
 
789
            def blame_receiver(line_no, revision, author, date, line, pool):
 
790
                annotations.append(revision)
 
791
            try:
 
792
                rev = _svn_rev(self.rev)
 
793
                start = _svn_rev(0)
 
794
                repo_url = 'file:///%s/%s' % (self.repos.path.lstrip('/'),
 
795
                                              self._scoped_svn_path)
 
796
                self.repos.log.info('opening ra_local session to ' + repo_url)
 
797
                from svn import client
 
798
                client.blame2(repo_url, rev, start, rev, blame_receiver,
 
799
                              client.create_context(), self.pool())
 
800
            except (core.SubversionException, AttributeError), e:
 
801
                # svn thinks file is a binary or blame not supported
 
802
                raise TracError(_('svn blame failed: %(error)s',
 
803
                                  error=to_unicode(e)))
 
804
        return annotations
 
805
 
605
806
#    def get_previous(self):
606
807
#        # FIXME: redo it with fs.node_history
607
808
 
624
825
        return self._get_prop(core.SVN_PROP_MIME_TYPE)
625
826
 
626
827
    def get_last_modified(self):
627
 
        date = fs.revision_prop(self.fs_ptr, self.created_rev,
628
 
                                core.SVN_PROP_REVISION_DATE, self.pool())
629
 
        if not date:
630
 
            return 0
631
 
        return core.svn_time_from_cstring(date, self.pool()) / 1000000
 
828
        _date = fs.revision_prop(self.fs_ptr, self.created_rev,
 
829
                                 core.SVN_PROP_REVISION_DATE, self.pool())
 
830
        if not _date:
 
831
            return None
 
832
        ts = core.svn_time_from_cstring(_date, self.pool()) / 1000000
 
833
        return datetime.fromtimestamp(ts, utc)
632
834
 
633
835
    def _get_prop(self, name):
634
 
        return fs.node_prop(self.root, self._scoped_svn_path, name, self.pool())
 
836
        return fs.node_prop(self.root, self._scoped_svn_path, name,
 
837
                            self.pool())
635
838
 
636
839
 
637
840
class SubversionChangeset(Changeset):
650
853
        # we _hope_ it's UTF-8, but can't be 100% sure (#4321)
651
854
        message = message and to_unicode(message, 'utf-8')
652
855
        author = author and to_unicode(author, 'utf-8')
653
 
        date = self._get_prop(core.SVN_PROP_REVISION_DATE)
654
 
        if date:
655
 
            date = core.svn_time_from_cstring(date, self.pool()) / 1000000
 
856
        _date = self._get_prop(core.SVN_PROP_REVISION_DATE)
 
857
        if _date:
 
858
            ts = core.svn_time_from_cstring(_date, self.pool()) / 1000000
 
859
            date = datetime.fromtimestamp(ts, utc)
656
860
        else:
657
 
            date = 0
 
861
            date = None
658
862
        Changeset.__init__(self, rev, message, author, date)
659
863
 
 
864
    def get_properties(self):
 
865
        props = fs.revision_proplist(self.fs_ptr, self.rev, self.pool())
 
866
        properties = {}
 
867
        for k,v in props.iteritems():
 
868
            if k not in (core.SVN_PROP_REVISION_LOG,
 
869
                         core.SVN_PROP_REVISION_AUTHOR,
 
870
                         core.SVN_PROP_REVISION_DATE):
 
871
                properties[k] = to_unicode(v)
 
872
                # Note: the above `to_unicode` has a small probability
 
873
                # to mess-up binary properties, like icons.
 
874
        return properties
 
875
 
660
876
    def get_changes(self):
661
877
        pool = Pool(self.pool)
662
878
        tmp = Pool(pool)
692
908
                        continue # duplicates on base_path are possible (#3778)
693
909
                    action = Changeset.DELETE
694
910
                    deletions[base_path] = idx
695
 
                elif self.scope:        # root property change
 
911
                elif self.scope == '/': # root property change
696
912
                    action = Changeset.EDIT
697
913
                else:                   # deletion outside of scope, ignore
698
914
                    continue
751
967
# Note 2: the 'dir_baton' is the path of the parent directory
752
968
#
753
969
 
754
 
class DiffChangeEditor(delta.Editor): 
755
 
 
756
 
    def __init__(self):
757
 
        self.deltas = []
 
970
 
 
971
def DiffChangeEditor():
 
972
 
 
973
    class DiffChangeEditor(delta.Editor): 
 
974
 
 
975
        def __init__(self):
 
976
            self.deltas = []
758
977
    
759
 
    # -- svn.delta.Editor callbacks
760
 
 
761
 
    def open_root(self, base_revision, dir_pool):
762
 
        return ('/', Changeset.EDIT)
763
 
 
764
 
    def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev,
765
 
                      dir_pool):
766
 
        self.deltas.append((path, Node.DIRECTORY, Changeset.ADD))
767
 
        return (path, Changeset.ADD)
768
 
 
769
 
    def open_directory(self, path, dir_baton, base_revision, dir_pool):
770
 
        return (path, dir_baton[1])
771
 
 
772
 
    def change_dir_prop(self, dir_baton, name, value, pool):
773
 
        path, change = dir_baton
774
 
        if change != Changeset.ADD:
775
 
            self.deltas.append((path, Node.DIRECTORY, change))
776
 
 
777
 
    def delete_entry(self, path, revision, dir_baton, pool):
778
 
        self.deltas.append((path, None, Changeset.DELETE))
779
 
 
780
 
    def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision,
781
 
                 dir_pool):
782
 
        self.deltas.append((path, Node.FILE, Changeset.ADD))
783
 
 
784
 
    def open_file(self, path, dir_baton, dummy_rev, file_pool):
785
 
        self.deltas.append((path, Node.FILE, Changeset.EDIT))
 
978
        # -- svn.delta.Editor callbacks
 
979
 
 
980
        def open_root(self, base_revision, dir_pool):
 
981
            return ('/', Changeset.EDIT)
 
982
 
 
983
        def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev,
 
984
                          dir_pool):
 
985
            self.deltas.append((path, Node.DIRECTORY, Changeset.ADD))
 
986
            return (path, Changeset.ADD)
 
987
 
 
988
        def open_directory(self, path, dir_baton, base_revision, dir_pool):
 
989
            return (path, dir_baton[1])
 
990
 
 
991
        def change_dir_prop(self, dir_baton, name, value, pool):
 
992
            path, change = dir_baton
 
993
            if change != Changeset.ADD:
 
994
                self.deltas.append((path, Node.DIRECTORY, change))
 
995
 
 
996
        def delete_entry(self, path, revision, dir_baton, pool):
 
997
            self.deltas.append((path, None, Changeset.DELETE))
 
998
 
 
999
        def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision,
 
1000
                     dir_pool):
 
1001
            self.deltas.append((path, Node.FILE, Changeset.ADD))
 
1002
 
 
1003
        def open_file(self, path, dir_baton, dummy_rev, file_pool):
 
1004
            self.deltas.append((path, Node.FILE, Changeset.EDIT))
 
1005
 
 
1006
    return DiffChangeEditor() 
786
1007