~ubuntu-branches/debian/sid/trac-bzr/sid

« back to all changes in this revision

Viewing changes to tracbzr/backend.py

  • Committer: Bazaar Package Importer
  • Author(s): Jelmer Vernooij
  • Date: 2009-12-25 15:37:57 UTC
  • mfrom: (1.1.3 upstream)
  • Revision ID: james.westby@ubuntu.com-20091225153757-n8v0lixk6pwxep36
Tags: 0.3.0-1
* New upstream release.
* Add watch file.

Show diffs side-by-side

added added

removed removed

Lines of Context:
37
37
import time
38
38
import urllib
39
39
import re
 
40
import fnmatch
40
41
 
41
42
import trac
42
 
from trac import versioncontrol, core, mimeview, wiki
 
43
from trac import versioncontrol, core, config, mimeview, wiki
43
44
from trac.web.chrome import Chrome
44
45
from trac.util.html import html, Markup
45
46
 
68
69
    revision,
69
70
    transport,
70
71
)
 
72
import bzrlib.api
71
73
from bzrlib.revision import (
72
74
    CURRENT_REVISION,
73
75
    NULL_REVISION,
74
76
    )
75
77
 
 
78
config_section = 'tracbzr'
76
79
 
77
80
class BzrConnector(core.Component):
78
 
    """The necessary glue between our repository and trac."""
79
 
 
80
 
    core.implements(versioncontrol.IRepositoryConnector,
81
 
                    wiki.IWikiMacroProvider,
82
 
                    IPropertyRenderer,
83
 
                    )
84
 
 
85
 
    # IRepositoryConnector
 
81
    """The necessary glue between our set of bzr branches and trac."""
 
82
 
 
83
    core.implements(versioncontrol.IRepositoryConnector)
 
84
 
 
85
    def __init__(self):
 
86
        if hasattr(self.env, 'systeminfo'):
 
87
            bzr_version = '%i.%i.%i' % bzrlib.api.get_current_api_version(None)
 
88
            self.env.systeminfo.append(('Bazaar', bzr_version))
86
89
 
87
90
    def get_supported_types(self):
88
91
        """Support for `repository_type = bzr`"""
89
92
        yield ('bzr', 8)
90
 
        yield ('bzr+debug', 8)
91
93
 
92
94
    def get_repository(self, repos_type, repos_dir, authname):
93
95
        """Return a `BzrRepository`"""
94
96
        assert repos_type == 'bzr'
95
 
        return BzrRepository(repos_dir, self.log)
96
 
 
97
 
    # IWikiMacroProvider
 
97
        return BzrRepository(repos_dir, self)
 
98
 
 
99
class BzrWikiMacros(core.Component):
 
100
    """Component for macros related to bzr."""
 
101
 
 
102
    core.implements(wiki.IWikiMacroProvider)
98
103
 
99
104
    def get_macros(self):
100
105
        yield 'Branches'
118
123
        if not isinstance(repo, BzrRepository):
119
124
            raise core.TracError('Configured repository is not a bzr repository')
120
125
        try:
121
 
            branches = sorted(repo._get_branches())
 
126
            branches = repo.get_branches()
122
127
            rows = []
123
128
            for loc, target in branches:
124
129
                revid = target.last_revision()
143
148
        finally:
144
149
            repo.close()
145
150
 
146
 
    # IPropertyRenderer
 
151
class BzrPropertyRenderer(core.Component):
 
152
    """Renderer for bzr-specific properties."""
 
153
 
 
154
    core.implements(IPropertyRenderer)
147
155
 
148
156
    def match_property(self, name, mode):
149
157
        if name == 'parents' and mode == 'revprop':
193
201
        self.unlock()
194
202
 
195
203
    def append(self, branch):
 
204
        branch.lock_read()
196
205
        self._locked_branches.append(branch)
197
206
 
198
207
    def unlock(self):
205
214
 
206
215
    """Present a bzr branch as a trac repository."""
207
216
 
208
 
    def __init__(self, location, log):
209
 
        versioncontrol.Repository.__init__(self, location, None, log)
 
217
    primary_branches = config.ListOption(
 
218
        config_section, 'primary_branches', ',trunk', keep_empty = True, 
 
219
        doc = """Ordered list of primary branches.
 
220
 
 
221
        These will be listed first in the Branches macro. When viewing
 
222
        the timeline, each changeset will be associated with the first
 
223
        primary branch that contains it. The value is a comma
 
224
        separated list of globs, as used by the fnmatch module. An
 
225
        empty list element can be used to denote the branch at the
 
226
        root of the repository.
 
227
 
 
228
        Defaults to ',trunk'.""")
 
229
 
 
230
    def __init__(self, location, component):
 
231
        versioncontrol.Repository.__init__(self, location, None, component.log)
 
232
        self.component = component
 
233
        self.config = component.config
210
234
        self.root_transport = transport.get_transport(location)
211
235
        self._tree_cache = {}
212
236
        self._locked_branches = LockedBranches()
235
259
        branch is None or a bzr branch.
236
260
        """
237
261
        if branch is None:
238
 
            # No "safe" chars (make sure "/" is escaped)
239
 
            return self._escape(revid, ':')
 
262
            return self._escape_revid(revid)
240
263
        relpath = self.branch_path(branch)
241
264
        try:
242
265
            return self._string_rev_revno(relpath, branch.revision_id_to_revno(revid))
244
267
            dotted = self.dotted_revno(branch, revid)
245
268
            if dotted is not None:
246
269
                return self._string_rev_revno(relpath, dotted)
247
 
            if revid in branch.repository.get_ancestry(branch.last_revision()):
 
270
            else:
248
271
                return self._string_rev_revid(relpath, revid)
249
 
            else:
250
 
                return self._escape(revid, ':')
251
 
 
252
 
    @staticmethod
253
 
    def _escape(string, safe=''):
254
 
        return urllib.quote(string, safe)
255
 
 
256
 
    @staticmethod
257
 
    def _string_rev_revid(relpath, revid):
258
 
        branch_name = urllib.quote(relpath, '')
259
 
        revid = urllib.quote(revid, '')
 
272
 
 
273
    @staticmethod
 
274
    def _escape_revid(revid):
 
275
        return urllib.quote(revid, ':')
 
276
 
 
277
    @staticmethod
 
278
    def _unescape_revid(revid):
 
279
        return urllib.unquote(revid)
 
280
 
 
281
    @staticmethod
 
282
    def _escape_branch(relpath):
 
283
        if '%' in relpath or ',' in relpath:
 
284
            relpath = urllib.quote(relpath, '')
 
285
        else:
 
286
            relpath = relpath.replace('/', ',')
 
287
        return relpath
 
288
 
 
289
    @staticmethod
 
290
    def _unescape_branch(relpath):
 
291
        if '%' in relpath:
 
292
            relpath = urllib.unquote(relpath)
 
293
        else:
 
294
            relpath = relpath.replace(',', '/')
 
295
        return relpath
 
296
 
 
297
    @classmethod
 
298
    def _string_rev_revid(cls, relpath, revid):
 
299
        branch_name = cls._escape_branch(relpath)
 
300
        revid = cls._escape_revid(revid)
260
301
        if branch_name:
261
302
            return '%s,%s' % (branch_name, revid)
262
303
        else:   
263
304
            return '%s' % revid
264
305
 
265
 
    @staticmethod
266
 
    def _string_rev_revno(relpath, revno):
267
 
        branch_name = urllib.quote(relpath, ':')
268
 
        if branch_name != "":
 
306
    @classmethod
 
307
    def _string_rev_revno(cls, relpath, revno):
 
308
        branch_name = cls._escape_branch(relpath)
 
309
        if branch_name:
269
310
            return '%s,%s' % (branch_name, revno)
270
311
        else:
271
312
            return str(revno)
283
324
           (currently revid is assumed to be in the branch ancestry!)
284
325
         - "spork," is latest revision in the spork branch.
285
326
         - "spork,1.2.3" is either revno or revid 1.2.3 in the spork branch.
 
327
         - "foo,bar,123" is revno 123 in the foo/bar branch.
286
328
 
287
 
        Branch paths and revids are urlencoded.
 
329
        Branch paths have / replaced by , and existing , escaped by doubling.
 
330
        Revids are urlencoded.
288
331
        """
289
332
 
290
333
        # Make sure our rev is a string
295
338
        if len(split) == 1:
296
339
            rev_branch = ''
297
340
            rev_rev = rev
298
 
        elif len(split) == 2:
299
 
            rev_branch, rev_rev = split
 
341
        elif len(split) >= 2:
 
342
            rev_branch = self._unescape_branch(','.join(split[:-1]))
 
343
            rev_rev = split[-1]
300
344
        else:
301
345
            raise versioncontrol.NoSuchChangeset(rev)
302
346
 
303
347
        # unquote revision part, and treat special cases of current: and null:
304
 
        rev_rev = urllib.unquote(rev_rev)
 
348
        rev_rev = self._unescape_revid(rev_rev)
305
349
        if len(split) == 1 and rev_rev in (CURRENT_REVISION, NULL_REVISION):
306
350
            return None, rev_rev
307
351
 
310
354
        except errors.NotBranchError:
311
355
            raise versioncontrol.NoSuchChangeset(rev)
312
356
 
313
 
        if rev_rev == '':
 
357
        if rev_rev == '' or rev_rev == CURRENT_REVISION:
314
358
            revid = branch.last_revision()
315
359
        elif rev_rev.isdigit():
316
360
            try:
341
385
        if location in self._branch_cache:
342
386
            return self._branch_cache[location].branch
343
387
        trans = self.root_transport
344
 
        for piece in urllib.unquote(location).split('/'):
 
388
        for piece in location.split('/'):
345
389
            if piece:
346
390
                trans = trans.clone(piece)
347
391
        target_bzrdir = bzrdir.BzrDir.open_from_transport(trans)
348
392
        branch = target_bzrdir.open_branch()
349
 
        branch.lock_read()
350
393
        self._locked_branches.append(branch)
351
394
        self._branch_cache[location] = BranchCache(self, branch)
352
395
        return branch
356
399
        real_location = location[:-len(relpath)].rstrip('/')
357
400
        if real_location not in self._branch_cache:
358
401
            self._branch_cache[real_location] = BranchCache(self, branch)
359
 
            branch.lock_read()
360
402
            self._locked_branches.append(branch)
361
403
        # return the cached version, possibly throwing away the one we just
362
404
        # retrieved.
363
405
        return self._branch_cache[real_location].branch, relpath
364
406
 
 
407
    def get_branches(self):
 
408
        """Get an ordered list of all branches in the repository.
 
409
 
 
410
        Returns a list of (relpath, branch) pairs. Branches will be
 
411
        locked for reading. Branches inside other branches will not be
 
412
        listed."""
 
413
        primary = self.primary_branches + ['*']
 
414
        branches = []
 
415
        for loc, branch in self._get_branches():
 
416
            for index, pattern in enumerate(primary):
 
417
                if fnmatch.fnmatch(loc, pattern):
 
418
                    branches.append((index, loc, branch))
 
419
                    break
 
420
        branches.sort()
 
421
        for index, loc, branch in branches:
 
422
            self._locked_branches.append(branch)
 
423
        return [(loc, branch) for index, loc, branch in branches]
 
424
 
365
425
    def _get_branches(self, trans=None, loc=()):
366
426
        """Find branches under a listable transport.
367
427
 
396
456
        except errors.NoSuchRevision, e:
397
457
            raise versioncontrol.NoSuchChangeset(rev)
398
458
 
399
 
    # TODO: get_changesets?
 
459
    def get_changesets(self, start, stop):
 
460
        """Retrieve all changesets in a cetain time span."""
 
461
        seen = set()
 
462
        rhss = [] # right hand side revids, one iterable for each branch
 
463
        inrange = []
 
464
        branches = self.get_branches()
 
465
        def walk(cache, revid):
 
466
            rhs = [] # all right hand side revids we encounter
 
467
            while revid not in seen:
 
468
                seen.add(revid)
 
469
                revision = cache.get_revision(revid)
 
470
                time = trac_timestamp(revision.timestamp)
 
471
                if time < start:
 
472
                    break
 
473
                if time < stop:
 
474
                    chgset = BzrChangeset(self, branch, revid, self.log)
 
475
                    inrange.append((time, revid, chgset))
 
476
                parents = revision.parent_ids
 
477
                if not parents:
 
478
                    break
 
479
                if len(parents) > 1:
 
480
                    rhs.extend(parents[1:])
 
481
                revid = parents[0]
 
482
            return rhs
 
483
        for relpath, branch in branches:
 
484
            cache = self.get_branch_cache(branch)
 
485
            revid = branch.last_revision()
 
486
            rhs = walk(cache, revid)
 
487
            rhss.append(rhs)
 
488
        for (relpath, branch), rhs in zip(branches, rhss):
 
489
            cache = self.get_branch_cache(branch)
 
490
            while rhs:
 
491
                revid = rhs.pop()
 
492
                new_rhs = walk(cache, revid)
 
493
                if new_rhs:
 
494
                    rhs.extend(new_rhs)
 
495
        inrange.sort()
 
496
        return [chgset for time, revid, chgset in inrange]
400
497
 
401
498
    def has_node(self, path, rev=None):
402
499
        """Return a boolean indicating if the node is present in a rev."""
411
508
        """Return a Node object or raise NoSuchNode or NoSuchChangeset."""
412
509
        path = self.normalize_path(path)
413
510
        if rev is None:
414
 
            rev = self._escape(CURRENT_REVISION)
 
511
            rev = self._escape_revid(CURRENT_REVISION)
415
512
        revbranch, revid = self._parse_rev(rev)
416
513
        try:
417
514
            branch, relpath = self.get_containing_branch(path)
418
515
        except errors.NotBranchError:
419
516
            if not self.root_transport.has(path):
420
517
                raise versioncontrol.NoSuchNode(path, rev)
421
 
            return UnversionedDirNode(self, path)
 
518
            return UnversionedDirNode(self, revbranch, revid, path)
422
519
        if revbranch is None:
423
520
            revbranch = branch
424
521
        try:
469
566
        revisions = sorted(revision_set, key=lambda x: x.timestamp)
470
567
        return [(r.revision_id, branches[r.revision_id]) for r in revisions]
471
568
 
472
 
    def previous_rev(self, rev):
 
569
    def previous_rev(self, rev, path=''):
 
570
        if path:
 
571
            prev = self.get_node(path, rev).get_previous()
 
572
            if prev is None:
 
573
                return None
 
574
            return prev[1]
473
575
        branch, revid = self._parse_rev(rev)
474
 
        if revid == NULL_REVISION:
 
576
        if revid in (NULL_REVISION, CURRENT_REVISION) or branch is None:
475
577
            return None
476
 
        if self._previous is None:
477
 
            if self._history is None:
478
 
                self._history = self._repo_history()
479
 
            self._previous = {}
480
 
            last = None
481
 
            for rev, branch in reversed(self._history):
482
 
                if rev == last:
483
 
                    raise repr(self._history)
484
 
                if last is not None:
485
 
                    self._previous[last] = (branch, rev)
486
 
                last = rev
487
 
        if revid == CURRENT_REVISION:
488
 
            return self.string_rev(self._history[-1][1], self._history[-1][0])
 
578
        ancestry = iter(branch.repository.iter_reverse_revision_history(revid))
 
579
        ancestry.next()
489
580
        try:
490
 
            return self.string_rev(*self._previous[revid])
491
 
        except KeyError:
492
 
            return NULL_REVISION 
 
581
            return self.string_rev(branch, ancestry.next())
 
582
        except StopIteration:
 
583
            return self.string_rev(branch, NULL_REVISION)
493
584
 
494
585
    def next_rev(self, rev, path=''):
495
586
        # TODO path is ignored.
497
588
        if revid == CURRENT_REVISION:
498
589
            return None
499
590
        if revid == NULL_REVISION:
500
 
            return CURRENT_REVISION
 
591
            if branch is None:
 
592
                return CURRENT_REVISION
 
593
            revno = 0
501
594
        if branch is None:
502
 
            ancestry = self.repo.get_ancestry(self.branch.last_revision())
503
 
        else:
504
 
            ancestry = branch.repository.get_ancestry(branch.last_revision())
505
 
        try:
506
 
            idx = ancestry.index(revid)
507
 
        except ValueError:
508
 
            # XXX this revision is not in the branch ancestry. Now what?
509
 
            return None
510
 
        try:
511
 
            next_revid = ancestry[idx + 1]
512
 
        except IndexError:
513
 
            # There is no next rev. Now what?
514
 
            return None
 
595
            return None
 
596
        try:
 
597
            revno = branch.revision_id_to_revno(revid)
 
598
        except errors.NoSuchRevision:
 
599
            return None # non-mainline branches don't have a future (yet).
 
600
        try:
 
601
            next_revid = branch.get_rev_id(revno + 1)
 
602
        except errors.NoSuchRevision:
 
603
            return None # this was the last revision on that branch
515
604
        return self.string_rev(branch, next_revid)
516
605
 
517
606
    def rev_older_than(self, rev1, rev2):
519
608
            return False
520
609
        branch1, rrev1 = self._parse_rev(rev1)
521
610
        branch2, rrev2 = self._parse_rev(rev2)
522
 
        if rrev2 == CURRENT_REVISION:
523
 
            return False
524
 
        first_before_second = rrev1 in branch2.repository.get_ancestry(rrev2)
525
 
        second_before_first = rrev2 in branch1.repository.get_ancestry(rrev1)
526
 
        if first_before_second and second_before_first:
527
 
            raise core.TracError('%s and %s precede each other?' %
528
 
                                 (rrev1, rrev2))
529
 
        if first_before_second:
530
 
            return True
531
 
        if second_before_first:
532
 
            return False
 
611
        if rrev1 == rrev2:
 
612
            return False
 
613
        both = frozenset([rrev1, rrev2])
 
614
        if NULL_REVISION in both:
 
615
            return NULL_REVISION == rrev1
 
616
        if CURRENT_REVISION in both:
 
617
            return CURRENT_REVISION == rrev2
 
618
 
 
619
        if branch1.repository.has_revisions(both) == both:
 
620
            heads = branch1.repository.get_graph().heads(both)
 
621
        elif branch2.repository.has_revisions(both) == both:
 
622
            heads = branch2.repository.get_graph().heads(both)
 
623
        else:
 
624
            heads = both
 
625
        if len(s) == 1:
 
626
            return rrev2 in heads
533
627
        # Bah, unrelated revisions. Fall back to comparing timestamps.
534
628
        return (branch1.repository.get_revision(rrev1).timestamp <
535
629
                branch2.repository.get_revision(rrev2).timestamp)
572
666
    def short_rev(self, rev):
573
667
        """Attempt to shorten a rev.
574
668
 
575
 
        This returns the revno if there is one, otherwise returns a
576
 
        "nearby" revno with a ~ prefix.
 
669
        This returns the last 6 characters of the dotted revno.
577
670
 
578
671
        The result of this method is used above the line number
579
672
        columns in the diff/changeset viewer. There is *very* little
580
 
        room there. Our ~revno results are actually a little too wide
581
 
        already. Tricks like using the branch nick simply do not fit.
 
673
        room there. Tricks like using the branch nick simply do not fit.
 
674
        
 
675
        Those 6 chars seem to fit, at least if two of them are dots,
 
676
        and should be enough to clearly identify almost any two
 
677
        revisions named in a changeset.
582
678
        """
583
679
        branch, revid = self._parse_rev(rev)
584
680
        if branch is None:
 
681
            return revid
 
682
        dotted = self.dotted_revno(branch, revid)
 
683
        if dotted is None:
585
684
            return '????'
586
 
        history = branch.revision_history()
587
 
        # First try if it is a revno.
588
 
        try:
589
 
            return str(history.index(revid) + 1)
590
 
        except ValueError:
591
 
            # Get the closest thing that *is* a revno.
592
 
            ancestry = branch.repository.get_ancestry(revid)
593
 
            # We've already tried the current one.
594
 
            ancestry.pop()
595
 
            for ancestor in reversed(ancestry):
596
 
                try:
597
 
                    return '~%s' % (history.index(ancestor) + 1,)
598
 
                except ValueError:
599
 
                    pass
600
 
        # XXX unrelated branch. Now what?
601
 
        return '????'
 
685
        return dotted[-6:]
602
686
 
603
687
    def get_changes(self, old_path, old_rev, new_path, new_rev,
604
688
                    ignore_ancestry=1):
675
759
 
676
760
 
677
761
class UnversionedDirNode(BzrNode):
678
 
    def __init__(self, bzr_repo, path):
679
 
        rev_string = urllib.quote(CURRENT_REVISION)
 
762
    def __init__(self, bzr_repo, branch, revid, path):
 
763
        rev_string = BzrRepository._escape_revid(CURRENT_REVISION)
680
764
        BzrNode.__init__(self, path, rev_string, versioncontrol.Node.DIRECTORY)
681
765
        self.transport = bzr_repo.root_transport.clone(path)
682
766
        self.bzr_repo = bzr_repo
 
767
        self.branch = branch
 
768
        self.revid = revid
683
769
        self.path = path
684
770
 
685
771
    def __repr__(self):
701
787
            try:
702
788
                branch = self.bzr_repo.get_branch(child_path)
703
789
            except errors.NotBranchError:
704
 
                result.append(UnversionedDirNode(self.bzr_repo, child_path))
 
790
                result.append(UnversionedDirNode(self.bzr_repo, self.branch,
 
791
                                                 self.revid, child_path))
705
792
            else:
706
793
                tree = branch.basis_tree()
707
794
                if tree.inventory.root:
708
795
                    node = BzrDirNode(self.bzr_repo, branch, tree,
709
796
                                      tree.inventory.root, child_path)
710
797
                else:
711
 
                    node = UnversionedDirNode(self.bzr_repo, child_path)
 
798
                    node = UnversionedDirNode(self.bzr_repo, self.branch,
 
799
                                              self.revid, child_path)
712
800
                result.append(node)
713
801
        return result
714
802
 
722
810
        return 'application/octet-stream'
723
811
 
724
812
    def get_history(self, limit=None):
725
 
        return [(self.path, BzrRepository._escape(CURRENT_REVISION), 'add')]
 
813
        if self.branch is not None:
 
814
            bpath = self.bzr_repo.branch_path(self.branch)
 
815
            if self.path == '' or bpath.startswith(self.path + '/'):
 
816
                repo = self.branch.repository
 
817
                ancestry = repo.iter_reverse_revision_history(self.revid)
 
818
                ancestry = iter(ancestry)
 
819
                for rev_id in ancestry:
 
820
                    yield (self.path,
 
821
                           self.bzr_repo.string_rev(self.branch, rev_id),
 
822
                           versioncontrol.Changeset.EDIT)
 
823
                yield (self.path,
 
824
                       self.bzr_repo.string_rev(self.branch, NULL_REVISION),
 
825
                       versioncontrol.Changeset.ADD)
 
826
                return
 
827
        yield (self.path, BzrRepository._escape_revid(CURRENT_REVISION), 'add')
 
828
        return
726
829
 
727
830
 
728
831
class BzrVersionedNode(BzrNode):