~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): Chris Lamb
  • Date: 2007-03-09 03:58:13 UTC
  • Revision ID: james.westby@ubuntu.com-20070309035813-ouzb56usywzt0s7q
Tags: upstream-0.2+bzr31
ImportĀ upstreamĀ versionĀ 0.2+bzr31

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: iso-8859-1 -*-
 
2
#
 
3
# Copyright (C) 2005 Edgewall Software
 
4
# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
 
5
# Copyright (C) 2005 Johan Rydberg <jrydberg@gnu.org>
 
6
# Copyright (C) 2006 Yann Hodique <hodique@lifl.fr>
 
7
# Copyright (C) 2006 Lukas Lalinsky <lalinsky@gmail.com>
 
8
# Copyright (C) 2006 Marien Zwart <marienz@gentoo.org>
 
9
# All rights reserved.
 
10
#
 
11
# This software may be used and distributed according to the terms
 
12
# of the GNU General Public License, incorporated herein by reference.
 
13
#
 
14
# Author: Yann Hodique <hodique@lifl.fr>
 
15
 
 
16
 
 
17
"""Bazaar-ng backend for trac's versioncontrol."""
 
18
 
 
19
 
 
20
import datetime
 
21
import StringIO
 
22
from itertools import izip
 
23
import time
 
24
import urllib
 
25
 
 
26
from trac import versioncontrol, core, mimeview, wiki
 
27
from trac.util.html import html, Markup
 
28
 
 
29
from bzrlib import (
 
30
    branch as bzrlib_branch, 
 
31
    bzrdir, 
 
32
    errors, 
 
33
    inventory, 
 
34
    osutils,
 
35
    revision,
 
36
    transport,
 
37
    tsort,
 
38
)
 
39
 
 
40
 
 
41
class BzrConnector(core.Component):
 
42
 
 
43
    """The necessary glue between our repository and trac."""
 
44
 
 
45
    core.implements(versioncontrol.IRepositoryConnector,
 
46
                    wiki.IWikiMacroProvider)
 
47
 
 
48
    # IRepositoryConnector
 
49
 
 
50
    def get_supported_types(self):
 
51
        """Support for `repository_type = bzr`"""
 
52
        yield ('bzr', 8)
 
53
        yield ('bzr+debug', 8)
 
54
 
 
55
    def get_repository(self, repos_type, repos_dir, authname):
 
56
        """Return a `BzrRepository`"""
 
57
        assert repos_type in ('bzr', 'bzr+debug')
 
58
        if repos_type == 'bzr+debug':
 
59
            # HACK: this causes logging to be applied to all BzrRepositories,
 
60
            # BzrChangesets, and BzrNodes
 
61
            BzrRepository.__getattribute__ = getattribute
 
62
            BzrChangeset.__getattribute__ = getattribute
 
63
            BzrNode.__getattribute__ = getattribute            
 
64
        return BzrRepository(repos_dir, self.log)
 
65
 
 
66
    # IWikiMacroProvider
 
67
 
 
68
    def get_macros(self):
 
69
        yield 'Branches'
 
70
 
 
71
    def get_macro_description(self, name):
 
72
        assert name == 'Branches'
 
73
        return 'Render a list of available branches.'
 
74
 
 
75
    def render_macro(self, req, name, content):
 
76
        assert name == 'Branches'
 
77
        # This is pretty braindead but adding an option for this is too.
 
78
        manager = versioncontrol.RepositoryManager(self.env)
 
79
        if manager.repository_type != 'bzr':
 
80
            raise core.TracError('Configured repo is not a bzr repo')
 
81
        temp_branch = bzrlib_branch.Branch.open(manager.repository_dir)
 
82
        trans = temp_branch.repository.bzrdir.root_transport
 
83
        branches = sorted(self._get_branches(trans))
 
84
        # Slight hack. We know all these branches are in the same
 
85
        # repo, so we can read lock that once.
 
86
        repo = bzrdir.BzrDir.open_from_transport(trans).open_repository()
 
87
        repo.lock_read()
 
88
        try:
 
89
            return html.TABLE(class_='listing')(
 
90
                html.THEAD(html.TR(
 
91
                        html.TH('Path'), html.TH('Nick'),
 
92
                        html.TH('Last Change'))),
 
93
                html.TBODY([
 
94
                        html.TR(
 
95
                            html.TD(html.A(loc, href=req.href.browser(
 
96
                                        rev=':%s' % (urllib.quote(loc, ''),
 
97
                                                     )))),
 
98
                            html.TD(target.nick),
 
99
                            html.TD(
 
100
                                datetime.datetime.fromtimestamp(
 
101
                                    repo.get_revision(
 
102
                                        target.last_revision()).timestamp
 
103
                                    ).ctime()),
 
104
                            )
 
105
                        for loc, target in branches]))
 
106
        finally:
 
107
            repo.unlock()
 
108
 
 
109
 
 
110
class BzrRepository(versioncontrol.Repository):
 
111
 
 
112
    """Present a bzr branch as a trac repository."""
 
113
 
 
114
    def __init__(self, location, log):
 
115
        versioncontrol.Repository.__init__(self, location, None, log)
 
116
        self.root_transport = transport.get_transport(location)
 
117
        self._tree_cache = {}
 
118
        self._locked_branches = []
 
119
        self._branch_cache = {}
 
120
        self._history = None
 
121
        self._previous = None
 
122
        self._revision_cache = {}
 
123
 
 
124
    def __repr__(self):
 
125
        return 'BzrRepository(%r)' % self.root_transport.base
 
126
 
 
127
    def branch_path(self, branch):
 
128
        """Determine the relative path to a branch from the root"""
 
129
        repo_path = self.root_transport.base
 
130
        branch_path = branch.bzrdir.root_transport.base
 
131
        if branch_path.startswith(repo_path):
 
132
            return branch_path[len(repo_path):].rstrip('/')
 
133
        else:
 
134
            repo_path = osutil.normalizepath(repo_path)
 
135
            branch_path = osutil.normalizepath(branch_path)
 
136
            return osutils.relpath(repo_path, branch_path)
 
137
 
 
138
    def string_rev(self, branch, revid):
 
139
        """Create a trac rev string.
 
140
 
 
141
        branch is None or a bzr branch.
 
142
        """
 
143
        if branch is None:
 
144
            # No "safe" chars (make sure "/" is escaped)
 
145
            return self._escape(revid)
 
146
        relpath = self.branch_path(branch)
 
147
        try:
 
148
            return '%s,%s' % (urllib.quote(relpath, ':'),
 
149
                              branch.revision_id_to_revno(revid))
 
150
        except errors.NoSuchRevision:
 
151
            dotted = self.dotted_revno(branch, revid)
 
152
            if dotted is not None:
 
153
                return '%s,%s' % (urllib.quote(relpath, ':'), dotted)
 
154
            if revid in branch.repository.get_ancestry(branch.last_revision()):
 
155
                return '%s,%s' % (urllib.quote(relpath, ''),
 
156
                                  self._escape(revid, ':'))
 
157
            else:
 
158
                return self._escape(revid, ':')
 
159
 
 
160
    @staticmethod
 
161
    def _escape(string):
 
162
        return urllib.quote(string, '')
 
163
 
 
164
    @staticmethod
 
165
    def _string_rev_revid(relpath, revid):
 
166
        return '%s,%s' % (urllib.quote(relpath, ''), urllib.quote(revid, ''))
 
167
 
 
168
    def _parse_rev(self, rev):
 
169
        """Translate a trac rev string into a (branch, revid) tuple.
 
170
 
 
171
        branch is None or a bzr branch object.
 
172
 
 
173
        Supported syntax:
 
174
         - "spork,123" is revno 123 in the spork branch.
 
175
         - "spork,revid" is a revid in the spork branch.
 
176
           (currently revid is assumed to be in the branch ancestry!)
 
177
 
 
178
        Branch paths and revids are urlencoded.
 
179
        """
 
180
        # Try integer revno to revid conversion.
 
181
        if rev.isdigit():
 
182
            raise versioncontrol.NoSuchChangeset(rev)
 
183
 
 
184
        # Try path-to-branch-in-repo.
 
185
        if ',' in rev:
 
186
            split = rev.split(',')
 
187
            if len(split) != 2:
 
188
                raise versioncontrol.NoSuchChangeset(rev)
 
189
            rev_branch,rev_rev = split
 
190
            try:
 
191
                branch = self.get_branch(rev_branch)
 
192
            except errors.NotBranchError:
 
193
                raise versioncontrol.NoSuchChangeset(rev)
 
194
 
 
195
            if len(split) == 2:
 
196
                if rev_rev.isdigit():
 
197
                    try:
 
198
                        revid = branch.get_rev_id(int(rev_rev))
 
199
                    except errors.NoSuchRevision:
 
200
                        raise versioncontrol.NoSuchChangeset(rev)
 
201
                else:
 
202
                    dotted = rev_rev.split('.')
 
203
                    for segment in dotted:
 
204
                        if not segment.isdigit():
 
205
                            revid = urllib.unquote(rev_rev)
 
206
                            break
 
207
                    else:
 
208
                        cache = self.get_branch_cache(branch)
 
209
                        revid = cache.revid_from_dotted(rev_rev)
 
210
                        if revid is None:
 
211
                            raise repr(dotted)
 
212
                            revid = urllib.unquote(rev_rev)
 
213
            else:
 
214
                revid = branch.last_revision()
 
215
 
 
216
            return branch, revid
 
217
 
 
218
        # Try raw revid.
 
219
        revid = urllib.unquote(rev)
 
220
        if revid in ('current:', 'null:'):
 
221
            return None, revid
 
222
        return None, revid
 
223
        if self.repo.has_revision(revid):
 
224
            return None, revid
 
225
 
 
226
        # Unsupported format.
 
227
        raise versioncontrol.NoSuchChangeset(rev)
 
228
 
 
229
    def __del__(self):
 
230
        # XXX Eeeeeww. Unfortunately for us trac does not actually call the
 
231
        # close method. So we do this. Quite silly, since bzr does the same
 
232
        # thing (printing a warning...)
 
233
        self.close()
 
234
 
 
235
    # Trac api methods.
 
236
 
 
237
    def close(self):
 
238
        """Release our branches. Trac does not *have* to call this!"""
 
239
        for branch in self._locked_branches:
 
240
            branch.unlock()
 
241
 
 
242
    def get_branch(self, location):
 
243
        if location in self._branch_cache:
 
244
            return self._branch_cache[location].branch
 
245
        trans = self.root_transport
 
246
        for piece in urllib.unquote(location).split('/'):
 
247
            if piece:
 
248
                trans = trans.clone(piece)
 
249
        target_bzrdir = bzrdir.BzrDir.open_from_transport(trans)
 
250
        branch = target_bzrdir.open_branch()
 
251
        branch.lock_read()
 
252
        self._locked_branches.append(branch)
 
253
        self._branch_cache[location] = BranchCache(self, branch)
 
254
        return branch
 
255
 
 
256
    def get_containing_branch(self, location):
 
257
        branch, relpath = containing_branch(self.root_transport, location)
 
258
        real_location = location[:-len(relpath)].rstrip('/')
 
259
        if real_location not in self._branch_cache:
 
260
            self._branch_cache[real_location] = BranchCache(self, branch)
 
261
            branch.lock_read()
 
262
            self._locked_branches.append(branch)
 
263
        # return the cached version, possibly throwing away the one we just
 
264
        # retrieved.
 
265
        return self._branch_cache[real_location].branch, relpath
 
266
 
 
267
    def _get_branches(self, trans=None, loc=()):
 
268
        """Find branches under a listable transport.
 
269
 
 
270
        Does not descend into control directories or branch directories.
 
271
        (branches inside other branches will not be listed)
 
272
        """
 
273
        if trans is None:
 
274
            trans = self.root_transport
 
275
        try:
 
276
            children = trans.list_dir('.')
 
277
            if '.bzr' in children: 
 
278
                children.remove('.bzr')
 
279
                try:
 
280
                    target_bzrdir = bzrdir.BzrDir.open_from_transport(trans)
 
281
                    yield '/'.join(loc), target_bzrdir.open_branch()
 
282
                except errors.NotBranchError:
 
283
                    pass
 
284
                else:
 
285
                    return
 
286
            for child in children:
 
287
                for child_loc, child_branch in self._get_branches(
 
288
                    trans.clone(child), loc + (child,)):
 
289
                    yield child_loc, child_branch
 
290
        except errors.NoSuchFile, e:
 
291
            return
 
292
 
 
293
    def get_changeset(self, rev):
 
294
        """Retrieve a Changeset."""
 
295
        branch, revid = self._parse_rev(rev)
 
296
        try:
 
297
            return BzrChangeset(self, branch, revid, self.log)
 
298
        except errors.NoSuchRevision, e:
 
299
            assert e.revision == revid
 
300
            raise versioncontrol.NoSuchChangeset(rev)
 
301
 
 
302
    # TODO: get_changesets?
 
303
 
 
304
    def has_node(self, path, rev=None):
 
305
        """Return a boolean indicating if the node is present in a rev."""
 
306
        try:
 
307
            self.get_node(path, rev)
 
308
        except versioncontrol.NoSuchNode:
 
309
            return False
 
310
        else:
 
311
            return True
 
312
 
 
313
    def get_node(self, path, rev=None):
 
314
        """Return a Node object or raise NoSuchNode or NoSuchChangeset."""
 
315
        path = self.normalize_path(path)
 
316
        if rev is None:
 
317
            rev = 'current%3A'
 
318
        revbranch, revid = self._parse_rev(rev)
 
319
        try:
 
320
            branch, relpath = self.get_containing_branch(path)
 
321
        except errors.NotBranchError:
 
322
            if not self.root_transport.has(path):
 
323
                raise versioncontrol.NoSuchNode(path, rev)
 
324
            return UnversionedDirNode(self, path)
 
325
        if revbranch is None:
 
326
            revbranch = branch
 
327
        try:
 
328
            if revid == 'current:':
 
329
                tree = revbranch.basis_tree()
 
330
            else:
 
331
                tree = revbranch.repository.revision_tree(revid)
 
332
        except (errors.NoSuchRevision, errors.RevisionNotPresent):
 
333
            raise versioncontrol.NoSuchChangeset(rev)
 
334
        file_id = tree.inventory.path2id(relpath)
 
335
        if file_id is None:
 
336
            raise versioncontrol.NoSuchNode(path, rev)
 
337
        entry = tree.inventory[file_id]
 
338
        klass = NODE_MAP[entry.kind]
 
339
        return klass(self, branch, tree, entry, path)
 
340
 
 
341
    def get_oldest_rev(self):
 
342
        # TODO just use revno here
 
343
        # (by definition, this revision is always in the branch history)
 
344
        return self.string_rev(None, 'null:')
 
345
 
 
346
    def get_youngest_rev(self):
 
347
        # TODO just use revno here
 
348
        # (by definition, this revision is always in the branch history)
 
349
        return self.string_rev(None, 'current:')
 
350
 
 
351
    def _repo_history(self):
 
352
        revisions = {}
 
353
        repos = {}
 
354
        branches = {}
 
355
        seen = set()
 
356
        for loc, branch in self._get_branches():
 
357
            repo_base = branch.repository.bzrdir.transport.base
 
358
            repos[repo_base] = branch.repository
 
359
            for revision_id in reversed(branch.revision_history()):
 
360
                if revision_id in seen:
 
361
                    break
 
362
                revisions.setdefault(repo_base, []).append(revision_id)
 
363
                branches[revision_id] = branch
 
364
                seen.add(revision_id)
 
365
        revision_set = set()
 
366
        for repo_base, revision_ids in revisions.iteritems():
 
367
            revision_set.update(repos[repo_base].get_revisions(revision_ids))
 
368
        revisions = sorted(revision_set, key=lambda x: x.timestamp)
 
369
        return [(r.revision_id, branches[r.revision_id]) for r in revisions]
 
370
 
 
371
    def previous_rev(self, rev):
 
372
        branch, revid = self._parse_rev(rev)
 
373
        if revid == 'null:':
 
374
            return None
 
375
        if self._history is None:
 
376
            self._history = self._repo_history()
 
377
            self._previous = {}
 
378
            last = None
 
379
            for rev, branch in reversed(self._history):
 
380
                if rev == last:
 
381
                    raise repr(self._history)
 
382
                if last is not None:
 
383
                    self._previous[last] = (branch, rev)
 
384
                last = rev
 
385
        if revid == 'current:':
 
386
            return self.string_rev(self._history[-1][1], self._history[-1][0])
 
387
        try:
 
388
            return self.string_rev(*self._previous[revid])
 
389
        except KeyError:
 
390
            return 'null:'
 
391
 
 
392
    def next_rev(self, rev, path=''):
 
393
        # TODO path is ignored.
 
394
        branch, revid = self._parse_rev(rev)
 
395
        if revid == 'current:':
 
396
            return None
 
397
        if revid == 'null:':
 
398
            return 'current:'
 
399
        if branch is None:
 
400
            ancestry = self.repo.get_ancestry(self.branch.last_revision())
 
401
        else:
 
402
            ancestry = branch.repository.get_ancestry(branch.last_revision())
 
403
        try:
 
404
            idx = ancestry.index(revid)
 
405
        except ValueError:
 
406
            # XXX this revision is not in the branch ancestry. Now what?
 
407
            return None
 
408
        try:
 
409
            next_revid = ancestry[idx + 1]
 
410
        except IndexError:
 
411
            # There is no next rev. Now what?
 
412
            return None
 
413
        return self.string_rev(branch, next_revid)
 
414
 
 
415
    def rev_older_than(self, rev1, rev2):
 
416
        if rev1 == rev2:
 
417
            return False
 
418
        branch1, rrev1 = self._parse_rev(rev1)
 
419
        branch2, rrev2 = self._parse_rev(rev2)
 
420
        if rrev2 == 'current:':
 
421
            return False
 
422
        first_before_second = rrev1 in branch2.repo.get_ancestry(rrev2)
 
423
        second_before_first = rrev2 in branch1.repo.get_ancestry(rrev1)
 
424
        if first_before_second and second_before_first:
 
425
            raise core.TracError('%s and %s precede each other?' %
 
426
                                 (rrev1, rrev2))
 
427
        if first_before_second:
 
428
            return True
 
429
        if second_before_first:
 
430
            return False
 
431
        # Bah, unrelated revisions. Fall back to comparing timestamps.
 
432
        return (self.repo.get_revision(rrev1).timestamp <
 
433
                self.repo.get_revision(rrev2).timestamp)
 
434
 
 
435
    # XXX what is get_youngest_rev_in_cache doing in here
 
436
 
 
437
    def get_path_history(self, path, rev=None, limit=None):
 
438
        """Shortcut for Node's get_history."""
 
439
        # XXX I think the intention for this one is probably different:
 
440
        # it should track the state of this filesystem location across time.
 
441
        # That is, it should keep tracking the same path as stuff is moved
 
442
        # on to / away from that path.
 
443
 
 
444
        # No need to normalize/unquote, get_node is a trac api method
 
445
        # so it takes quoted values.
 
446
        return self.get_node(path, rev).get_history(limit)
 
447
 
 
448
    def normalize_path(self, path):
 
449
        """Remove leading and trailing '/'"""
 
450
        # Also turns None into '', just in case.
 
451
        return path and path.strip('/') or ''
 
452
 
 
453
    def normalize_rev(self, rev):
 
454
        """Turn a user-specified rev into a "normalized" rev.
 
455
 
 
456
        This turns None into a rev, and may convert a revid-based rev into
 
457
        a revno-based one.
 
458
        """
 
459
        if rev is None:
 
460
            branch = None
 
461
            revid = 'current:'
 
462
        else:
 
463
            branch, revid = self._parse_rev(rev)
 
464
        if branch is not None:
 
465
            repository = branch.repository
 
466
        else:
 
467
            repository = None
 
468
        return self.string_rev(branch, revid)
 
469
 
 
470
    def short_rev(self, rev):
 
471
        """Attempt to shorten a rev.
 
472
 
 
473
        This returns the revno if there is one, otherwise returns a
 
474
        "nearby" revno with a ~ prefix.
 
475
 
 
476
        The result of this method is used above the line number
 
477
        columns in the diff/changeset viewer. There is *very* little
 
478
        room there. Our ~revno results are actually a little too wide
 
479
        already. Tricks like using the branch nick simply do not fit.
 
480
        """
 
481
        branch, revid = self._parse_rev(rev)
 
482
        if branch is None:
 
483
            return '????'
 
484
        history = branch.revision_history()
 
485
        # First try if it is a revno.
 
486
        try:
 
487
            return str(history.index(revid) + 1)
 
488
        except ValueError:
 
489
            # Get the closest thing that *is* a revno.
 
490
            ancestry = branch.repository.get_ancestry(revid)
 
491
            # We've already tried the current one.
 
492
            ancestry.pop()
 
493
            for ancestor in reversed(ancestry):
 
494
                try:
 
495
                    return '~%s' % (history.index(ancestor) + 1,)
 
496
                except ValueError:
 
497
                    pass
 
498
        # XXX unrelated branch. Now what?
 
499
        return '????'
 
500
 
 
501
    def get_changes(self, old_path, old_rev, new_path, new_rev,
 
502
                    ignore_ancestry=1):
 
503
        """yields (old_node, new_node, kind, change) tuples."""
 
504
        # ignore_ancestry is ignored, don't know what it's for.
 
505
        if old_path != new_path:
 
506
            raise core.TracError(
 
507
                'Currently the bzr plugin does not support this between '
 
508
                'different directories. Sorry.')
 
509
        old_branch, old_revid = self._parse_rev(old_rev)
 
510
        new_branch, new_revid = self._parse_rev(new_rev)
 
511
        old_tree = old_branch.repository.revision_tree(old_revid)
 
512
        new_tree = new_branch.repository.revision_tree(new_revid)
 
513
        delta = new_tree.changes_from(old_tree)
 
514
        for path, file_id, kind in delta.added:
 
515
            entry = new_tree.inventory[file_id]
 
516
            node = NODE_MAP[kind](self, new_branch, new_tree, entry, path)
 
517
            cur_path = new_tree.id2path(file_id)
 
518
            node._history_cache[(new_revid, old_revid, file_id)] = \
 
519
                cur_path, new_rev, versioncontrol.Changeset.ADD
 
520
            yield None, node, node.kind, versioncontrol.Changeset.ADD
 
521
        for path, file_id, kind in delta.removed:
 
522
            entry = old_tree.inventory[file_id]
 
523
            node = NODE_MAP[kind](self, old_branch, old_tree, entry, path)
 
524
            yield node, None, node.kind, versioncontrol.Changeset.DELETE
 
525
        for oldpath, newpath, file_id, kind, textmod, metamod in delta.renamed:
 
526
            oldnode = NODE_MAP[kind](self, old_branch, old_tree,
 
527
                                     old_tree.inventory[file_id], oldpath)
 
528
            newnode = NODE_MAP[kind](self, new_branch, new_tree,
 
529
                                     new_tree.inventory[file_id], newpath)
 
530
            if oldnode.kind != newnode.kind:
 
531
                raise core.TracError(
 
532
                    '%s changed kinds, I do not know how to handle that' % (
 
533
                        newpath,))
 
534
            yield oldnode, newnode, oldnode.kind, versioncontrol.Changeset.MOVE
 
535
        for path, file_id, kind, textmod, metamod in delta.modified:
 
536
            # Bzr won't report a changed path as a rename but trac wants that.
 
537
            oldpath = old_tree.id2path(file_id)
 
538
            oldnode = NODE_MAP[kind](self, old_branch, old_tree,
 
539
                                     old_tree.inventory[file_id], oldpath)
 
540
            newnode = NODE_MAP[kind](self, new_branch, new_tree,
 
541
                                     new_tree.inventory[file_id], path)
 
542
            if oldnode.kind != newnode.kind:
 
543
                raise core.TracError(
 
544
                    '%s changed kinds, I do not know how to handle that' % (
 
545
                        newpath,))
 
546
            if oldpath != path:
 
547
                action = versioncontrol.Changeset.MOVE
 
548
            else:
 
549
                action = versioncontrol.Changeset.EDIT
 
550
            cur_path = new_tree.id2path(file_id)
 
551
            newnode._history_cache[(new_revid, old_revid, file_id)] = \
 
552
                cur_path, new_rev, action
 
553
            yield oldnode, newnode, oldnode.kind, action
 
554
 
 
555
    def dotted_revno(self, branch, revid):
 
556
        return self.get_branch_cache(branch).dotted_revno(revid)
 
557
 
 
558
    def get_branch_cache(self, branch):
 
559
        branch_key = branch.bzrdir.root_transport.base
 
560
        if branch_key not in self._branch_cache:
 
561
            self._branch_cache[branch_key] = BranchCache(self, branch)
 
562
        return self._branch_cache[branch_key]
 
563
 
 
564
    def sorted_revision_history(self, branch):
 
565
        return self.get_branch_cache(branch).sorted_revision_history()
 
566
 
 
567
    def sync(self):
 
568
        """Dummy to satisfy interface requirements"""
 
569
        # XXX should we be dumping in-mem caches?  Seems unlikely.
 
570
        self.log = None
 
571
        pass
 
572
        
 
573
 
 
574
class BzrNode(versioncontrol.Node):
 
575
    pass
 
576
 
 
577
 
 
578
class UnversionedDirNode(BzrNode):
 
579
    def __init__(self, bzr_repo, path):
 
580
        rev_string = urllib.quote('current:')
 
581
        BzrNode.__init__(self, path, rev_string, versioncontrol.Node.DIRECTORY)
 
582
        self.transport = bzr_repo.root_transport.clone(path)
 
583
        self.bzr_repo = bzr_repo
 
584
        self.path = path
 
585
 
 
586
    def __repr__(self):
 
587
        return 'UnversionedDirNode(path=%r)' % self.path
 
588
 
 
589
    def get_properties(self):
 
590
        return {}
 
591
 
 
592
    def get_entries(self):
 
593
        result = []
 
594
        for name in self.transport.list_dir(''):
 
595
            if name == '.bzr':
 
596
                continue
 
597
            stat_mode = self.transport.stat(name).st_mode
 
598
            kind = osutils.file_kind_from_stat_mode(stat_mode)
 
599
            if not kind == 'directory':
 
600
                continue
 
601
            child_path = osutils.pathjoin(self.path, name)
 
602
            try:
 
603
                branch = self.bzr_repo.get_branch(child_path)
 
604
            except errors.NotBranchError:
 
605
                result.append(UnversionedDirNode(self.bzr_repo, child_path))
 
606
            else:
 
607
                tree = branch.basis_tree()
 
608
                node = BzrDirNode(self.bzr_repo, branch, tree, 
 
609
                                  tree.inventory.root, child_path)
 
610
                result.append(node)
 
611
        return result
 
612
 
 
613
    def get_content_length(self):
 
614
        return 0 
 
615
 
 
616
    def get_content(self):
 
617
        return StringIO.StringIO('')
 
618
 
 
619
    def get_content_type(self):
 
620
        return 'application/octet-stream'
 
621
 
 
622
    def get_history(self, limit=None):
 
623
        return [(self.path, 'current%3A', 'add')]
 
624
 
 
625
 
 
626
def sorted_revision_history(branch, generate_revno=False):
 
627
    history = branch.revision_history()
 
628
    graph = branch.repository.get_revision_graph(history[-1])
 
629
    return tsort.merge_sort(graph, history[-1], generate_revno=generate_revno)
 
630
 
 
631
 
 
632
class BzrVersionedNode(BzrNode):
 
633
 
 
634
    _history_cache = {}
 
635
    _diff_map = {
 
636
        'modified': versioncontrol.Changeset.EDIT,
 
637
        'unchanged': versioncontrol.Changeset.EDIT,
 
638
        'added': versioncontrol.Changeset.ADD,
 
639
        inventory.InventoryEntry.RENAMED: versioncontrol.Changeset.MOVE,
 
640
        inventory.InventoryEntry.MODIFIED_AND_RENAMED: 
 
641
            versioncontrol.Changeset.MOVE
 
642
    }
 
643
 
 
644
    def __init__(self, bzr_repo, branch, revisiontree, entry, path):
 
645
        """Initialize. path has to be a normalized path."""
 
646
        rev_string = bzr_repo.string_rev(branch, entry.revision)
 
647
        BzrNode.__init__(self, path, rev_string, self.kind)
 
648
        self.bzr_repo = bzr_repo
 
649
        self.log = bzr_repo.log
 
650
        self.repo = branch.repository
 
651
        self.branch = branch
 
652
        self.tree = revisiontree
 
653
        self.entry = entry
 
654
        # XXX I am not sure if this makes any sense but it does make
 
655
        # the links in the changeset viewer work.
 
656
        self.created_rev = self.rev
 
657
        self.created_path = self.path
 
658
        self.root_path = path[:-len(self.tree.id2path(self.entry.file_id))]
 
659
 
 
660
    def get_properties(self):
 
661
        # Must at least return an empty dict here (base class version raises).
 
662
        result = {}
 
663
        if self.entry.executable:
 
664
            result['executable'] = 'True'
 
665
        return result
 
666
 
 
667
    def _merging_history(self):
 
668
        """Iterate through history revisions that merged changes to this node
 
669
        
 
670
        This includes all revisions in which the revision_id changed.
 
671
        It may also include a few revisions in which the revision_id did not
 
672
        change, if the modification was subsequently undone.
 
673
        """
 
674
        weave = self.tree.get_weave(self.entry.file_id)
 
675
        file_ancestry = weave.get_ancestry(self.entry.revision)
 
676
        # Can't use None here, because it's a legitimate revision id.
 
677
        last_yielded = 'bogus:'
 
678
        for num, revision_id, depth, revno, eom in \
 
679
            self.bzr_repo.sorted_revision_history(self.branch):
 
680
            if depth == 0:
 
681
                last_mainline = revision_id
 
682
            if last_mainline == last_yielded:
 
683
                continue
 
684
            if revision_id in file_ancestry:
 
685
                yield last_mainline
 
686
                last_yielded = last_mainline
 
687
        yield None
 
688
 
 
689
    def get_history(self, limit=None):
 
690
        """Backward history.
 
691
 
 
692
        yields (path, revid, chg) tuples.
 
693
 
 
694
        path is the path to this entry. 
 
695
        
 
696
        revid is the revid string.  It is the revision in which the change
 
697
        was applied to the branch, not necessarily the revision that originated
 
698
        the change.  In SVN terms, it is a changeset, not a file revision.
 
699
        
 
700
        chg is a Changeset.ACTION thing.
 
701
 
 
702
        First thing should be for the current revision.
 
703
 
 
704
        limit is an int cap on how many entries to return.
 
705
        """
 
706
        history_iter = self._get_history()
 
707
        if limit is None:
 
708
            return history_iter
 
709
        else:
 
710
            return (y for x, y in izip(range(limit), history_iter))
 
711
 
 
712
    def _get_history(self, limit=None):
 
713
        file_id = self.entry.file_id
 
714
        revision = None
 
715
        history = list(self._merging_history())
 
716
        cache = self.bzr_repo.get_branch_cache(self.branch)
 
717
        trees = cache.revision_trees(history)
 
718
        for prev_tree in trees:
 
719
            previous_revision = prev_tree.get_revision_id()
 
720
            try:
 
721
                prev_file_revision = prev_tree.inventory[file_id].revision
 
722
            except errors.NoSuchId:
 
723
                prev_file_revision = None
 
724
            if (revision is not None and 
 
725
                prev_file_revision != file_revision):
 
726
                path, rev_str, chg = \
 
727
                    self.get_change(revision, previous_revision, file_id)
 
728
                branch_revision = self.bzr_repo.string_rev(self.branch, 
 
729
                                                           revision)
 
730
                yield (osutils.pathjoin(self.root_path, path), 
 
731
                       branch_revision, chg)
 
732
            if prev_file_revision is None:
 
733
                break
 
734
            revision = previous_revision
 
735
            file_revision = prev_file_revision
 
736
 
 
737
    def get_change(self, revision, previous_revision, file_id):
 
738
        key = (revision, previous_revision, file_id)
 
739
        if key not in self._history_cache or False:
 
740
            self._history_cache[key] = self.calculate_history(revision, 
 
741
                previous_revision, file_id)
 
742
        return self._history_cache[key]
 
743
 
 
744
    def calculate_history(self, revision, previous_revision, file_id):
 
745
        cache = self.bzr_repo.get_branch_cache(self.branch)
 
746
        tree = cache.revision_tree(revision)
 
747
        current_entry = tree.inventory[file_id]
 
748
        current_path = tree.id2path(file_id)
 
749
        if previous_revision not in (None, 'null:'):
 
750
            previous_tree = cache.revision_tree(previous_revision)
 
751
            previous_entry = previous_tree.inventory[file_id]
 
752
        else:
 
753
            previous_entry = None
 
754
        # We should only get revisions in the ancestry for which
 
755
        # we exist, so this should succeed..
 
756
        return self.compare_entries(current_path, current_entry, 
 
757
                                    previous_entry)
 
758
 
 
759
    def compare_entries(self, current_path, current_entry, previous_entry):
 
760
        diff = current_entry.describe_change(previous_entry, current_entry)
 
761
        rev = self.bzr_repo.string_rev(self.branch, current_entry.revision)
 
762
        try:
 
763
            return current_path, rev, self._diff_map[diff]
 
764
        except KeyError:
 
765
            raise Exception('unknown describe_change %r' % (diff,))
 
766
 
 
767
    def get_last_modified(self):
 
768
        return self.tree.get_file_mtime(self.entry.file_id)
 
769
 
 
770
 
 
771
class BzrDirNode(BzrVersionedNode):
 
772
 
 
773
    isdir = True
 
774
    isfile = False
 
775
    kind = versioncontrol.Node.DIRECTORY
 
776
 
 
777
    def __init__(self, bzr_repo, branch, revisiontree, entry, path,
 
778
                 revcache=None):
 
779
        BzrVersionedNode.__init__(self, bzr_repo, branch, revisiontree, entry, 
 
780
                                  path)
 
781
        if revcache is None:
 
782
            ancestry = self.repo.get_ancestry(revisiontree.get_revision_id())
 
783
            ancestry.reverse()
 
784
            self.revcache = {}
 
785
            best = self._get_cache(self.revcache, ancestry, entry)
 
786
            self._orig_rev = ancestry[best]
 
787
            self.rev = bzr_repo.string_rev(self.branch, (ancestry[best]))
 
788
        else:
 
789
            self.revcache = revcache
 
790
            self._orig_rev = revcache[entry.file_id]
 
791
            self.rev = bzr_repo.string_rev(self.branch, self._orig_rev)
 
792
 
 
793
    def __repr__(self):
 
794
        return 'BzrDirNode(path=%r, relpath=%r)' % (self.path, self.entry.name)
 
795
 
 
796
    @classmethod
 
797
    def _get_cache(cls, cache, ancestry, entry, ancestry_idx=None):
 
798
        """Populate a file_id <-> revision_id mapping.
 
799
        
 
800
        This mapping is different from InventoryEntry.revision, but only for
 
801
        directories.  In this scheme, directories are considered modified
 
802
        if their contents are modified.
 
803
 
 
804
        The revision ids are not guaranteed to be in the mainline revision
 
805
        history.
 
806
 
 
807
        cache: The cache to populate
 
808
        ancestry: A topologically-sorted list of revisions, with more recent
 
809
            revisions having lower indexes.
 
810
        entry: The InventoryEntry to start at
 
811
        ancestry_idx: A mapping of revision_id <-> ancestry index.
 
812
        """
 
813
        if ancestry_idx is None:
 
814
            ancestry_idx = dict((r, n) for n, r in enumerate(ancestry))
 
815
        # best ~= most recent revision to modify a child of this directory
 
816
        best = ancestry_idx[entry.revision]
 
817
        for child in entry.children.itervalues():
 
818
            if child.kind == 'directory':
 
819
                index = cls._get_cache(cache, ancestry, child, ancestry_idx)
 
820
                cache[child.file_id] = ancestry[index]
 
821
            else:
 
822
                index = ancestry_idx[child.revision]
 
823
            best = min(best, index)
 
824
        return best
 
825
 
 
826
    def get_content(self):
 
827
        """Return a file-like (read(length)) for a file, None for a dir."""
 
828
        return None
 
829
 
 
830
    def get_entries(self):
 
831
        """Yield child Nodes if a dir, return None if a file."""
 
832
        for name, entry in self.entry.children.iteritems():
 
833
            childpath = '/'.join((self.path, name))
 
834
            klass = NODE_MAP[entry.kind]
 
835
            if klass is BzrDirNode:
 
836
                yield klass(self.bzr_repo, self.branch, self.tree, entry,
 
837
                            childpath, self.revcache)
 
838
            else:
 
839
                yield klass(self.bzr_repo, self.branch, self.tree, entry,
 
840
                            childpath)
 
841
 
 
842
    def get_content_length(self):
 
843
        return None
 
844
 
 
845
    def get_content_type(self):
 
846
        return None
 
847
 
 
848
    def _get_revision_history(self, limit=None):
 
849
        history = self.branch.revision_history()
 
850
        first = history[0]
 
851
        if limit is not None:
 
852
            history = history[-limit:]
 
853
        self.bzr_repo.get_branch_cache(self.branch).cache_revisions(history)
 
854
        for rev_id in reversed(history):
 
855
            if rev_id == first:
 
856
                operation = versioncontrol.Changeset.ADD
 
857
            else:
 
858
                operation = versioncontrol.Changeset.EDIT
 
859
            yield (self.path, 
 
860
                   self.bzr_repo.string_rev(self.branch, rev_id),
 
861
                   operation)
 
862
 
 
863
    def get_history(self, limit=None):
 
864
        """Backward history.
 
865
 
 
866
        yields (path, rev, chg) tuples.
 
867
 
 
868
        path is the path to this entry, rev is the revid string.
 
869
        chg is a Changeset.ACTION thing.
 
870
 
 
871
        First thing should be for the current revision.
 
872
 
 
873
        This is special because it checks for changes recursively,
 
874
        not just to this directory. bzr only treats the dir as changed
 
875
        if it is renamed, not if its contents changed. Trac needs
 
876
        this recursive behaviour.
 
877
 
 
878
        limit is an int cap on how many entries to return.
 
879
        """
 
880
        current_entry = self.entry
 
881
        file_id = current_entry.file_id
 
882
        if current_entry.parent_id == None:
 
883
            for r in self._get_revision_history(limit):
 
884
                yield r
 
885
            return
 
886
            
 
887
        count = 0
 
888
        # We need the rev we were created with, not the rev the entry
 
889
        # specifies (our contents may have changed between that rev
 
890
        # and our own current rev).
 
891
        current_revid = self._orig_rev
 
892
 
 
893
        if self.branch is not None:
 
894
            history = self.branch.revision_history()
 
895
        else:
 
896
            history = []
 
897
        if current_revid == 'current:':
 
898
            current_revid = history[-1]
 
899
        # If the current_revid we start from is in the branch history,
 
900
        # limit our view to just the history, not the full ancestry.
 
901
        try:
 
902
            index = history.index(current_revid)
 
903
        except ValueError:
 
904
            ancestry = self.repo.get_ancestry(current_revid)
 
905
            # The last entry is this rev, skip it.
 
906
            ancestry.pop()
 
907
            ancestry.reverse()
 
908
            # The last entry is None, skip it.
 
909
            ancestry.pop()
 
910
        else:
 
911
            ancestry = ['null:'] + history[:index]
 
912
            ancestry.reverse()
 
913
        # Load a bunch of trees in one go. We do not know how many we
 
914
        # need: we may end up skipping some trees because they do not
 
915
        # change us.
 
916
        chunksize = limit or 100
 
917
        current_tree = self.tree
 
918
        current_path = current_tree.id2path(file_id)
 
919
        path_prefix = self.path[:-len(current_path)]
 
920
        while ancestry:
 
921
            chunk, ancestry = ancestry[:chunksize], ancestry[chunksize:]
 
922
            cache = self.bzr_repo.get_branch_cache(self.branch)
 
923
            for previous_revid, previous_tree in izip(
 
924
                chunk, cache.revision_trees(chunk)):
 
925
                if file_id in previous_tree.inventory:
 
926
                    previous_entry = previous_tree.inventory[file_id]
 
927
                else:
 
928
                    previous_entry = None
 
929
                delta = current_tree.changes_from(
 
930
                    previous_tree, specific_files=[current_path])
 
931
                if not delta.has_changed():
 
932
                    current_entry = previous_entry
 
933
                    current_path = previous_tree.inventory.id2path(file_id)
 
934
                    current_revid = previous_revid
 
935
                    current_tree = previous_tree
 
936
                    continue
 
937
                diff = current_entry.describe_change(previous_entry,
 
938
                                                     current_entry)
 
939
                if diff == 'added':
 
940
                    yield (path_prefix+current_path,
 
941
                           self.bzr_repo.string_rev(self.branch, 
 
942
                                                    current_revid),
 
943
                           versioncontrol.Changeset.ADD)
 
944
                    # There is no history before this point, we're done.
 
945
                    return
 
946
                elif diff == 'modified' or diff == 'unchanged':
 
947
                    # We want the entry anyway.
 
948
                    yield (path_prefix+current_path,
 
949
                           self.bzr_repo.string_rev(self.branch,
 
950
                                                    current_revid),
 
951
                           versioncontrol.Changeset.EDIT)
 
952
                elif diff in (current_entry.RENAMED,
 
953
                              current_entry.MODIFIED_AND_RENAMED):
 
954
                    yield (path_prefix+current_path,
 
955
                           self.bzr_repo.string_rev(self.branch, 
 
956
                                      current_revid),
 
957
                           versioncontrol.Changeset.MOVE)
 
958
                else:
 
959
                    raise Exception('unknown describe_change %r' % (diff,))
 
960
                count += 1
 
961
                if limit is not None and count >= limit:
 
962
                    return
 
963
                current_entry = previous_entry
 
964
                current_path = previous_tree.inventory.id2path(file_id)
 
965
                current_revid = previous_revid
 
966
                current_tree = previous_tree
 
967
 
 
968
    def get_previous(self):
 
969
        """Equivalent to i=iter(get_history(2));i.next();return i.next().
 
970
 
 
971
        The default implementation does essentially that, but we specialcase
 
972
        it because we can skip the loading of all the trees.
 
973
        """
 
974
        # Special case: if this is the root node it (well, its
 
975
        # contents) change every revision.
 
976
        if not self.tree.id2path(self.entry.file_id):
 
977
            return self.path, self.rev, versioncontrol.Changeset.EDIT
 
978
        if self._orig_rev != self.entry.revision:
 
979
            # The last change did not affect this dir directly, it changed
 
980
            # our contents.
 
981
            return self.path, self.rev, versioncontrol.Changeset.EDIT
 
982
        # We were affected directly. Get a delta to figure out how.
 
983
        delta = self.repo.get_revision_delta(self._orig_rev)
 
984
        for path, file_id, kind in delta.added:
 
985
            if file_id == self.entry.file_id:
 
986
                return path, self.rev, versioncontrol.Changeset.ADD
 
987
        for oldpath, newpath, file_id, kind, textmod, metamod in delta.renamed:
 
988
            if file_id == self.entry.file_id:
 
989
                return newpath, self.rev, versioncontrol.Changeset.MOVE
 
990
        # We were removed (which does not make any sense,
 
991
        # the tree we were constructed from is newer and has us)
 
992
        raise core.TracError('should not get here, %r %r %r' %
 
993
                             (self.entry, delta, self._orig_rev))
 
994
 
 
995
 
 
996
class BzrFileNode(BzrVersionedNode):
 
997
 
 
998
    isfile = True
 
999
    isdir = False
 
1000
    kind = versioncontrol.Node.FILE
 
1001
 
 
1002
    def __repr__(self):
 
1003
        return 'BzrFileNode(path=%r)' % self.path
 
1004
 
 
1005
    def get_content(self):
 
1006
        """Return a file-like (read(length)) for a file, None for a dir."""
 
1007
        return self.tree.get_file(self.entry.file_id)
 
1008
 
 
1009
    def get_entries(self):
 
1010
        return None
 
1011
 
 
1012
    def get_content_length(self):
 
1013
        return self.entry.text_size
 
1014
 
 
1015
    def get_content_type(self):
 
1016
        return mimeview.get_mimetype(self.name)
 
1017
 
 
1018
 
 
1019
class BzrSymlinkNode(BzrVersionedNode):
 
1020
 
 
1021
    """Kinda like a file only not really. Empty, properties only."""
 
1022
 
 
1023
    isfile = True
 
1024
    isdir = False
 
1025
    kind = versioncontrol.Node.FILE
 
1026
 
 
1027
    def __repr__(self):
 
1028
        return 'BzrSymlinkNode(path=%r)' % self.path
 
1029
 
 
1030
    def get_content(self):
 
1031
        return StringIO.StringIO('')
 
1032
 
 
1033
    def get_entries(self):
 
1034
        return None
 
1035
 
 
1036
    def get_content_length(self):
 
1037
        return 0
 
1038
 
 
1039
    def get_content_type(self):
 
1040
        return 'text/plain'
 
1041
 
 
1042
    def get_properties(self):
 
1043
        return {'destination': self.entry.symlink_target}
 
1044
 
 
1045
 
 
1046
NODE_MAP = {
 
1047
    'directory': BzrDirNode,
 
1048
    'file': BzrFileNode,
 
1049
    'symlink': BzrSymlinkNode,
 
1050
    }
 
1051
 
 
1052
 
 
1053
class BzrChangeset(versioncontrol.Changeset):
 
1054
 
 
1055
    def __init__(self, bzr_repo, branch, revid, log):
 
1056
        """Initialize from a bzr repo, an unquoted revid and a logger."""
 
1057
        self.log = log
 
1058
        self.bzr_repo = bzr_repo
 
1059
        if branch is None:
 
1060
            if revid in ('current:', 'null:'):
 
1061
                self.revision = revision.Revision(revid, committer='', 
 
1062
                                                  message='', timezone='')
 
1063
                versioncontrol.Changeset.__init__(self, urllib.quote(revid),
 
1064
                                                  '', '', time.time())
 
1065
            else:
 
1066
                raise errors.NoSuchRevision(None, revid)
 
1067
        else:
 
1068
            self.revision = bzr_repo.get_branch_cache(branch).get_revision(revid)
 
1069
            versioncontrol.Changeset.__init__(self,
 
1070
                                              bzr_repo.string_rev(
 
1071
                                              branch, revid),
 
1072
                                              self.revision.message,
 
1073
                                              self.revision.committer,
 
1074
                                              self.revision.timestamp)
 
1075
        self.branch = branch
 
1076
 
 
1077
    def __repr__(self):
 
1078
        return 'BzrChangeset(%r)' % (self.revision.revision_id)
 
1079
 
 
1080
    def get_properties(self):
 
1081
        """Return an iterator of (name, value, is wikitext, html class)."""
 
1082
        for name, value in self.revision.properties.iteritems():
 
1083
            yield name, value, False, ''
 
1084
        if len(self.revision.parent_ids) > 1:
 
1085
            for name, link in [('parent trees', ' * source:@%s'),
 
1086
                               ('changesets', ' * [changeset:%s]')]:
 
1087
                yield name, '\n'.join(
 
1088
                    link % (self.bzr_repo.string_rev(self.branch, parent),)
 
1089
                    for parent in self.revision.parent_ids), True, ''
 
1090
 
 
1091
    def get_changes(self):
 
1092
        """Yield changes.
 
1093
 
 
1094
        Return tuples are (path, kind, change, base_path, base_rev).
 
1095
        change is self.ADD/COPY/DELETE/EDIT/MOVE.
 
1096
        kind is Node.FILE or Node.DIRECTORY.
 
1097
        base_path and base_rev are the location and revision of the file
 
1098
        before "ours".
 
1099
        """
 
1100
        if self.revision.revision_id in ('current:', 'null:'):
 
1101
            return
 
1102
        branchpath = osutils.relpath(
 
1103
            osutils.normalizepath(self.bzr_repo.root_transport.base),
 
1104
            osutils.normalizepath(self.branch.bzrdir.root_transport.base))
 
1105
        for path, kind, change, base_path, base_rev in self._get_changes():
 
1106
            if path is not None:
 
1107
                path = osutils.pathjoin(branchpath, path)
 
1108
            if base_path is not None:
 
1109
                base_path = osutils.pathjoin(branchpath, base_path)
 
1110
            yield (path, kind, change, base_path, base_rev)
 
1111
 
 
1112
    def _get_changes(self):
 
1113
        if self.revision.revision_id in ('current:', 'null:'):
 
1114
            return
 
1115
        this = self.branch.repository.revision_tree(self.revision.revision_id)
 
1116
        parents = self.revision.parent_ids
 
1117
        if parents:
 
1118
            parent_revid = parents[0]
 
1119
        else:
 
1120
            parent_revid = None
 
1121
        other = self.branch.repository.revision_tree(parent_revid)
 
1122
        delta = this.changes_from(other)
 
1123
 
 
1124
        kindmap = {'directory': versioncontrol.Node.DIRECTORY,
 
1125
                   'file': versioncontrol.Node.FILE,
 
1126
                   'symlink': versioncontrol.Node.FILE, # gotta do *something*
 
1127
                   }
 
1128
 
 
1129
        # We have to make sure our base_path/base_rev combination
 
1130
        # exists (get_node succeeds). If we use
 
1131
        # other.inventory[file_id].revision as base_rev we cannot use
 
1132
        # what bzr hands us or what's in other.inventory as base_path
 
1133
        # since the parent node may have been renamed between what we
 
1134
        # return as base_rev and the revision the "other" inventory
 
1135
        # corresponds to (parent_revid).
 
1136
        # So we can either return other.id2path(file_id) and parent_revid
 
1137
        # or use entry.revision and pull up the inventory for that revision
 
1138
        # to get the path. Currently the code does the former,
 
1139
        # remember to update the paths if it turns out returning the other
 
1140
        # revision works better.
 
1141
 
 
1142
        for path, file_id, kind in delta.added:
 
1143
            # afaict base_{path,rev} *should* be ignored for this one.
 
1144
            yield path, kindmap[kind], self.ADD, None, None
 
1145
        for path, file_id, kind in delta.removed:
 
1146
            yield (path, kindmap[kind], self.DELETE, path,
 
1147
                   self.bzr_repo.string_rev(self.branch, parent_revid))
 
1148
        for oldpath, newpath, file_id, kind, textmod, metamod in delta.renamed:
 
1149
            yield (newpath, kindmap[kind], self.MOVE, other.id2path(file_id),
 
1150
                   self.bzr_repo.string_rev(self.branch, parent_revid))
 
1151
        for path, file_id, kind, textmod, metamod in delta.modified:
 
1152
            # "path" may not be accurate for base_path: the directory
 
1153
            # it is in may have been renamed. So pull the right path from
 
1154
            # the old inventory.
 
1155
            yield (path, kindmap[kind], self.EDIT, other.id2path(file_id),
 
1156
                   self.bzr_repo.string_rev(self.branch, parent_revid))
 
1157
 
 
1158
 
 
1159
def getattribute(self, attr):
 
1160
    """If a callable is requested log the call."""
 
1161
    obj = object.__getattribute__(self, attr)
 
1162
    if not callable(obj):
 
1163
        return obj
 
1164
    try:
 
1165
        log = object.__getattribute__(self, 'log')
 
1166
    except AttributeError:
 
1167
        return obj
 
1168
    if log is None:
 
1169
        return obj
 
1170
    else:
 
1171
        def _wrap(*args, **kwargs):
 
1172
            arg_list = [repr(a) for a in args] + \
 
1173
                ['%s=%r' % i for i in kwargs.iteritems() ]
 
1174
            call_str = '%s.%s(%s)' % \
 
1175
                (self, attr, ', '.join(arg_list))
 
1176
            if len(call_str) > 200:
 
1177
                call_str = '%s...' % call_str[:200]
 
1178
            log.debug('CALL %s' % call_str)
 
1179
            try:
 
1180
                result = obj(*args, **kwargs)
 
1181
            except Exception, e:
 
1182
                log.debug('FAILURE of %s: %s' % (call_str, e) )
 
1183
                raise
 
1184
            str_result = str(result)
 
1185
            if len(str_result) > 200:
 
1186
                str_result = '%s...' % str_result[:200]
 
1187
            log.debug('RESULT of %s: %s' % (call_str, str_result))
 
1188
            return result
 
1189
        return _wrap
 
1190
 
 
1191
 
 
1192
def containing_branch(transport, path):
 
1193
    child_transport = transport.clone(path)
 
1194
    my_bzrdir, relpath = \
 
1195
        bzrdir.BzrDir.open_containing_from_transport(child_transport)
 
1196
    return my_bzrdir.open_branch(), relpath
 
1197
 
 
1198
 
 
1199
class BranchCache(object):
 
1200
    
 
1201
    def __init__(self, bzr_repo, branch):
 
1202
        self.bzr_repo = bzr_repo
 
1203
        self.branch = branch
 
1204
        self._sorted_revision_history = None
 
1205
        self._dotted_revno = None
 
1206
        self._revno_revid = None
 
1207
 
 
1208
    def sorted_revision_history(self):
 
1209
        if self._sorted_revision_history is None:
 
1210
            self._sorted_revision_history = \
 
1211
                sorted_revision_history(self.branch, generate_revno=True)
 
1212
        return self._sorted_revision_history
 
1213
 
 
1214
    def _populate_dotted_maps(self):
 
1215
        if self._dotted_revno is None:
 
1216
            self._dotted_revno = {}
 
1217
            self._revno_revid = {}
 
1218
            for s, revision_id, m, revno, e in \
 
1219
                self.sorted_revision_history():
 
1220
                    dotted = '.'.join([str(s) for s in revno])
 
1221
                    self._dotted_revno[revision_id] = dotted
 
1222
                    self._revno_revid[dotted] = revision_id
 
1223
 
 
1224
    def dotted_revno(self, revid):
 
1225
        self._populate_dotted_maps()
 
1226
        return self._dotted_revno.get(revid)
 
1227
 
 
1228
    def revid_from_dotted(self, dotted_revno):
 
1229
        self._populate_dotted_maps()
 
1230
        return self._revno_revid.get(dotted_revno)
 
1231
 
 
1232
    def revision_tree(self, revision_id):
 
1233
        if revision_id not in self.bzr_repo._tree_cache:
 
1234
            self.bzr_repo._tree_cache[revision_id] = \
 
1235
                self.branch.repository.revision_tree(revision_id)
 
1236
            if revision_id == 'null:':
 
1237
                self.bzr_repo._tree_cache[None] = \
 
1238
                    self.bzr_repo._tree_cache['null:']
 
1239
        return self.bzr_repo._tree_cache[revision_id]
 
1240
 
 
1241
    def revision_trees(self, revision_ids):
 
1242
        if None in revision_ids or 'null:' in revision_ids:
 
1243
            self.revision_tree('null:')
 
1244
        missing = [r for r in revision_ids if r not in 
 
1245
                   self.bzr_repo._tree_cache]
 
1246
        if len(missing) > 0:
 
1247
            trees = self.branch.repository.revision_trees(missing)
 
1248
            for tree in trees:
 
1249
                self.bzr_repo._tree_cache[tree.get_revision_id()] = tree
 
1250
        return [self.bzr_repo._tree_cache[r] for r in revision_ids]
 
1251
 
 
1252
    def cache_revisions(self, revision_ids):
 
1253
        if self.bzr_repo.log:
 
1254
            self.bzr_repo.log.debug('caching %d revisions' % len(revision_ids))
 
1255
        missing = [r for r in revision_ids if r not in 
 
1256
                   self.bzr_repo._revision_cache]
 
1257
        revisions = self.branch.repository.get_revisions(missing)
 
1258
        self.bzr_repo._revision_cache.update(dict((r.revision_id, r) for r in
 
1259
                                                  revisions))
 
1260
        if self.bzr_repo.log:
 
1261
            self.bzr_repo.log.debug('done caching %d revisions' %
 
1262
                                    len(revision_ids))
 
1263
 
 
1264
    def get_revision(self, revision_id):
 
1265
        try:
 
1266
            return self.bzr_repo._revision_cache[revision_id]
 
1267
        except KeyError:
 
1268
            self.cache_revisions([revision_id])
 
1269
        return self.bzr_repo._revision_cache[revision_id]