~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-13 00:15:03 UTC
  • mto: This revision was merged to the branch mainline in revision 4.
  • Revision ID: james.westby@ubuntu.com-20091213001503-phgxwidfc75l1w3o
Tags: upstream-0.2+bzr83
ImportĀ upstreamĀ versionĀ 0.2+bzr83

Show diffs side-by-side

added added

removed removed

Lines of Context:
7
7
# Copyright (C) 2006 Lukas Lalinsky <lalinsky@gmail.com>
8
8
# Copyright (C) 2006 Marien Zwart <marienz@gentoo.org>
9
9
# Copyright (C) 2006, 2007 Panoramic Feedback <abentley@panoramicfeedback.com>
10
 
# Copyright (C) 2006,2008 Jelmer Vernooij <jelmer@samba.org>
 
10
# Copyright (C) 2006,2008,2009 Jelmer Vernooij <jelmer@samba.org>
 
11
# Copyright (C) 2009 Martin von Gagern <Martin.vGagern@gmx.net>
11
12
# All rights reserved.
12
13
#
13
14
# This program is free software; you can redistribute it and/or modify
27
28
# of the GNU General Public License, incorporated herein by reference.
28
29
 
29
30
 
30
 
"""Bazaar-ng backend for trac's versioncontrol."""
 
31
"""Bazaar backend for trac's versioncontrol."""
31
32
 
32
33
 
33
34
import datetime
35
36
from itertools import izip
36
37
import time
37
38
import urllib
 
39
import re
38
40
 
39
41
import trac
40
42
from trac import versioncontrol, core, mimeview, wiki
 
43
from trac.web.chrome import Chrome
41
44
from trac.util.html import html, Markup
42
 
if int(trac.__version__[2:4]) > 10:
 
45
 
 
46
trac_version = (int(trac.__version__[0]), int(trac.__version__[2:4]))
 
47
 
 
48
if trac_version >= (0,11):
43
49
    from trac.util.datefmt import utc
 
50
    # Check trac version.  trac 0.11 uses datetime objects for
 
51
    # timestamp information.
 
52
    def trac_timestamp(timestamp):
 
53
        return datetime.datetime.fromtimestamp(timestamp, utc)
 
54
    from trac.versioncontrol.web_ui.browser import IPropertyRenderer
 
55
else:
 
56
    def trac_timestamp(timestamp):
 
57
        return timestamp
 
58
    class IPropertyRenderer(core.Interface):
 
59
        pass
 
60
 
44
61
 
45
62
from bzrlib import (
46
63
    branch as bzrlib_branch, 
50
67
    osutils,
51
68
    revision,
52
69
    transport,
53
 
    tsort,
54
70
)
 
71
from bzrlib.revision import (
 
72
    CURRENT_REVISION,
 
73
    NULL_REVISION,
 
74
    )
55
75
 
56
76
 
57
77
class BzrConnector(core.Component):
58
 
 
59
78
    """The necessary glue between our repository and trac."""
60
79
 
61
80
    core.implements(versioncontrol.IRepositoryConnector,
62
 
                    wiki.IWikiMacroProvider)
 
81
                    wiki.IWikiMacroProvider,
 
82
                    IPropertyRenderer,
 
83
                    )
63
84
 
64
85
    # IRepositoryConnector
65
86
 
70
91
 
71
92
    def get_repository(self, repos_type, repos_dir, authname):
72
93
        """Return a `BzrRepository`"""
73
 
        assert repos_type in ('bzr', 'bzr+debug')
74
 
        if repos_type == 'bzr+debug':
75
 
            # HACK: this causes logging to be applied to all BzrRepositories,
76
 
            # BzrChangesets, and BzrNodes
77
 
            BzrRepository.__getattribute__ = getattribute
78
 
            BzrChangeset.__getattribute__ = getattribute
79
 
            BzrNode.__getattribute__ = getattribute            
 
94
        assert repos_type == 'bzr'
80
95
        return BzrRepository(repos_dir, self.log)
81
96
 
82
97
    # IWikiMacroProvider
90
105
 
91
106
    def render_macro(self, req, name, content):
92
107
        assert name == 'Branches'
93
 
        # This is pretty braindead but adding an option for this is too.
94
 
        manager = versioncontrol.RepositoryManager(self.env)
95
 
        if manager.repository_type != 'bzr':
96
 
            raise core.TracError('Configured repo is not a bzr repo')
97
 
        temp_branch = bzrlib_branch.Branch.open(manager.repository_dir)
98
 
        trans = temp_branch.repository.bzrdir.root_transport
99
 
        branches = sorted(self._get_branches(trans))
100
 
        # Slight hack. We know all these branches are in the same
101
 
        # repo, so we can read lock that once.
102
 
        repo = bzrdir.BzrDir.open_from_transport(trans).open_repository()
103
 
        repo.lock_read()
 
108
        if trac_version >= (0,11):
 
109
            chrome = Chrome(self.env)
 
110
            dateinfo = chrome.populate_data(req, {})['dateinfo']
 
111
        else:
 
112
            def dateinfo(timestamp):
 
113
                from trac.util.datefmt import format_datetime, pretty_timedelta
 
114
                date = format_datetime(timestamp)
 
115
                age = pretty_timedelta(timestamp)
 
116
                return html.SPAN(age, title=date)
 
117
        repo = self.env.get_repository(req.authname)
 
118
        if not isinstance(repo, BzrRepository):
 
119
            raise core.TracError('Configured repository is not a bzr repository')
104
120
        try:
 
121
            branches = sorted(repo._get_branches())
 
122
            rows = []
 
123
            for loc, target in branches:
 
124
                revid = target.last_revision()
 
125
                revno = target.revision_id_to_revno(revid)
 
126
                rev = repo.string_rev(target, revid)
 
127
                revision = target.repository.get_revision(revid)
 
128
                timestamp = revision.timestamp
 
129
                timestamp = dateinfo(timestamp)
 
130
                rows.append(html.TR([html.TD(x, class_=c) for c, x in [
 
131
                    ('name', html.A(loc, class_='dir',
 
132
                                    href=req.href.browser(loc))),
 
133
                    ('nick', target.nick),
 
134
                    ('rev', html.A('[%d]'%revno, href=req.href.changeset(rev))),
 
135
                    ('age', timestamp),
 
136
                    ('change', revision.message),
 
137
                    ]]))
105
138
            return html.TABLE(class_='listing')(
106
 
                html.THEAD(html.TR(
107
 
                        html.TH('Path'), html.TH('Nick'),
108
 
                        html.TH('Last Change'))),
 
139
                html.THEAD(html.TR([html.TH(x) for x in [
 
140
                    'Path', 'Nick', 'Revision', 'Age', 'Last Change'
 
141
                    ]])),
 
142
                html.TBODY(rows))
 
143
        finally:
 
144
            repo.close()
 
145
 
 
146
    # IPropertyRenderer
 
147
 
 
148
    def match_property(self, name, mode):
 
149
        if name == 'parents' and mode == 'revprop':
 
150
            return 4
 
151
        return 0
 
152
 
 
153
    def render_property(self, name, mode, context, props):
 
154
        if name == 'parents' and mode == 'revprop':
 
155
            path = None
 
156
            if context.resource.realm == 'changeset':
 
157
                try:
 
158
                    path = context.req.args.get('new_path')
 
159
                except KeyError:
 
160
                    pass
 
161
                if path is not None:
 
162
                    path = path.strip('/')
 
163
                if path == '':
 
164
                    path = None
 
165
            content = html.TABLE(class_='wiki parents')(
 
166
                html.THEAD(html.TR([html.TH(h) for h in
 
167
                                    ['Rev', 'Tree', 'Chgset']])),
109
168
                html.TBODY([
110
 
                        html.TR(
111
 
                            html.TD(html.A(loc, href=req.href.browser(
112
 
                                        rev=':%s' % (urllib.quote(loc, ''),
113
 
                                                     )))),
114
 
                            html.TD(target.nick),
115
 
                            html.TD(
116
 
                                datetime.datetime.fromtimestamp(
117
 
                                    repo.get_revision(
118
 
                                        target.last_revision()).timestamp
119
 
                                    ).ctime()),
120
 
                            )
121
 
                        for loc, target in branches]))
122
 
        finally:
123
 
            repo.unlock()
 
169
                    html.TR(
 
170
                        html.TD(p.split(',')[-1]),
 
171
                        html.TD(html.A('@%s' % p,
 
172
                                       href=context.href.browser(path, rev=p))),
 
173
                        html.TD(html.A('[%s]' % p,
 
174
                                       href=context.href.changeset(p, path))))
 
175
                    for p in props[name].split('\n')]))
 
176
            return content
 
177
 
 
178
 
 
179
class LockedBranches(object):
 
180
 
 
181
    """Keep track of locks and ensure they get unlocked one day.
 
182
    This deals with the fact that Trac doesn't always call the repository
 
183
    close method, and that a __del__ method in the repository itself causes
 
184
    trouble as described in https://bugs.launchpad.net/trac-bzr/+bug/484324"""
 
185
 
 
186
    def __init__(self):
 
187
        self._locked_branches = []
 
188
 
 
189
    def __del__(self):
 
190
        # XXX Eeeeeww. Unfortunately for us trac does not actually call the
 
191
        # close method of BzrRepository. So we do this. Quite silly, since
 
192
        # bzr does the same thing (printing a warning...)
 
193
        self.unlock()
 
194
 
 
195
    def append(self, branch):
 
196
        self._locked_branches.append(branch)
 
197
 
 
198
    def unlock(self):
 
199
        for branch in self._locked_branches:
 
200
            branch.unlock()
 
201
        self._locked_branches = []
124
202
 
125
203
 
126
204
class BzrRepository(versioncontrol.Repository):
131
209
        versioncontrol.Repository.__init__(self, location, None, log)
132
210
        self.root_transport = transport.get_transport(location)
133
211
        self._tree_cache = {}
134
 
        self._locked_branches = []
 
212
        self._locked_branches = LockedBranches()
135
213
        self._branch_cache = {}
136
214
        self._history = None
137
215
        self._previous = None
138
216
        self._revision_cache = {}
139
217
 
140
218
    def __repr__(self):
141
 
        return 'BzrRepository(%r)' % self.root_transport.base
 
219
        return '%s(%r)' % (self.__class__.__name__, self.root_transport.base)
142
220
 
143
221
    def branch_path(self, branch):
144
222
        """Determine the relative path to a branch from the root"""
147
225
        if branch_path.startswith(repo_path):
148
226
            return branch_path[len(repo_path):].rstrip('/')
149
227
        else:
150
 
            repo_path = osutil.normalizepath(repo_path)
151
 
            branch_path = osutil.normalizepath(branch_path)
 
228
            repo_path = osutils.normalizepath(repo_path)
 
229
            branch_path = osutils.normalizepath(branch_path)
152
230
            return osutils.relpath(repo_path, branch_path)
153
231
 
154
232
    def string_rev(self, branch, revid):
161
239
            return self._escape(revid, ':')
162
240
        relpath = self.branch_path(branch)
163
241
        try:
164
 
            return '%s,%s' % (urllib.quote(relpath, ':'),
165
 
                              branch.revision_id_to_revno(revid))
 
242
            return self._string_rev_revno(relpath, branch.revision_id_to_revno(revid))
166
243
        except errors.NoSuchRevision:
167
244
            dotted = self.dotted_revno(branch, revid)
168
245
            if dotted is not None:
169
 
                return '%s,%s' % (urllib.quote(relpath, ':'), dotted)
 
246
                return self._string_rev_revno(relpath, dotted)
170
247
            if revid in branch.repository.get_ancestry(branch.last_revision()):
171
248
                return self._string_rev_revid(relpath, revid)
172
249
            else:
178
255
 
179
256
    @staticmethod
180
257
    def _string_rev_revid(relpath, revid):
181
 
        return '%s,%s' % (urllib.quote(relpath, ''), urllib.quote(revid, ''))
 
258
        branch_name = urllib.quote(relpath, '')
 
259
        revid = urllib.quote(revid, '')
 
260
        if branch_name:
 
261
            return '%s,%s' % (branch_name, revid)
 
262
        else:   
 
263
            return '%s' % revid
 
264
 
 
265
    @staticmethod
 
266
    def _string_rev_revno(relpath, revno):
 
267
        branch_name = urllib.quote(relpath, ':')
 
268
        if branch_name != "":
 
269
            return '%s,%s' % (branch_name, revno)
 
270
        else:
 
271
            return str(revno)
182
272
 
183
273
    def _parse_rev(self, rev):
184
274
        """Translate a trac rev string into a (branch, revid) tuple.
186
276
        branch is None or a bzr branch object.
187
277
 
188
278
        Supported syntax:
 
279
         - "123" is revno 123 in the default branch.
 
280
         - "revid" is a revid in the default branch.
189
281
         - "spork,123" is revno 123 in the spork branch.
190
282
         - "spork,revid" is a revid in the spork branch.
191
283
           (currently revid is assumed to be in the branch ancestry!)
 
284
         - "spork," is latest revision in the spork branch.
 
285
         - "spork,1.2.3" is either revno or revid 1.2.3 in the spork branch.
192
286
 
193
287
        Branch paths and revids are urlencoded.
194
288
        """
195
 
        # Try integer revno to revid conversion.
196
 
        if rev.isdigit():
197
 
            raise versioncontrol.NoSuchChangeset(rev)
198
 
 
199
 
        # Try path-to-branch-in-repo.
200
 
        if ',' in rev:
201
 
            split = rev.split(',')
202
 
            if len(split) != 2:
203
 
                raise versioncontrol.NoSuchChangeset(rev)
 
289
 
 
290
        # Make sure our rev is a string
 
291
        rev=str(rev)
 
292
 
 
293
        # split branch,revid; no branch denotes branch at root of repository
 
294
        split = rev.split(',')
 
295
        if len(split) == 1:
 
296
            rev_branch = ''
 
297
            rev_rev = rev
 
298
        elif len(split) == 2:
204
299
            rev_branch, rev_rev = split
 
300
        else:
 
301
            raise versioncontrol.NoSuchChangeset(rev)
 
302
 
 
303
        # unquote revision part, and treat special cases of current: and null:
 
304
        rev_rev = urllib.unquote(rev_rev)
 
305
        if len(split) == 1 and rev_rev in (CURRENT_REVISION, NULL_REVISION):
 
306
            return None, rev_rev
 
307
 
 
308
        try:
 
309
            branch = self.get_branch(rev_branch)
 
310
        except errors.NotBranchError:
 
311
            raise versioncontrol.NoSuchChangeset(rev)
 
312
 
 
313
        if rev_rev == '':
 
314
            revid = branch.last_revision()
 
315
        elif rev_rev.isdigit():
205
316
            try:
206
 
                branch = self.get_branch(rev_branch)
207
 
            except errors.NotBranchError:
 
317
                revid = branch.get_rev_id(int(rev_rev))
 
318
            except errors.NoSuchRevision:
208
319
                raise versioncontrol.NoSuchChangeset(rev)
209
 
 
210
 
            if len(split) == 2:
211
 
                if rev_rev.isdigit():
212
 
                    try:
213
 
                        revid = branch.get_rev_id(int(rev_rev))
214
 
                    except errors.NoSuchRevision:
215
 
                        raise versioncontrol.NoSuchChangeset(rev)
216
 
                else:
217
 
                    dotted = rev_rev.split('.')
218
 
                    for segment in dotted:
219
 
                        if not segment.isdigit():
220
 
                            revid = urllib.unquote(rev_rev)
221
 
                            break
222
 
                    else:
223
 
                        cache = self.get_branch_cache(branch)
224
 
                        revid = cache.revid_from_dotted(rev_rev)
225
 
                        if revid is None:
226
 
                            raise repr(dotted)
227
 
                            revid = urllib.unquote(rev_rev)
 
320
        else:
 
321
            dotted = rev_rev.split('.')
 
322
            for segment in dotted:
 
323
                if not segment.isdigit():
 
324
                    revid = rev_rev
 
325
                    break
228
326
            else:
229
 
                revid = branch.last_revision()
230
 
 
231
 
            return branch, revid
232
 
 
233
 
        # Try raw revid.
234
 
        revid = urllib.unquote(rev)
235
 
        if revid in ('current:', 'null:'):
236
 
            return None, revid
237
 
        return None, revid
238
 
        if self.repo.has_revision(revid):
239
 
            return None, revid
240
 
 
241
 
        # Unsupported format.
242
 
        raise versioncontrol.NoSuchChangeset(rev)
243
 
 
244
 
    def __del__(self):
245
 
        # XXX Eeeeeww. Unfortunately for us trac does not actually call the
246
 
        # close method. So we do this. Quite silly, since bzr does the same
247
 
        # thing (printing a warning...)
248
 
        self.close()
 
327
                cache = self.get_branch_cache(branch)
 
328
                revid = cache.revid_from_dotted(rev_rev)
 
329
                if revid is None:
 
330
                    self.log.warn('%r is no dotted revno, will interpret as revid.', rev_rev)
 
331
                    revid = rev_rev
 
332
        return branch, revid
249
333
 
250
334
    # Trac api methods.
251
335
 
252
336
    def close(self):
253
337
        """Release our branches. Trac does not *have* to call this!"""
254
 
        for branch in self._locked_branches:
255
 
            branch.unlock()
 
338
        self._locked_branches.unlock()
256
339
 
257
340
    def get_branch(self, location):
258
341
        if location in self._branch_cache:
328
411
        """Return a Node object or raise NoSuchNode or NoSuchChangeset."""
329
412
        path = self.normalize_path(path)
330
413
        if rev is None:
331
 
            rev = 'current%3A'
 
414
            rev = self._escape(CURRENT_REVISION)
332
415
        revbranch, revid = self._parse_rev(rev)
333
416
        try:
334
417
            branch, relpath = self.get_containing_branch(path)
339
422
        if revbranch is None:
340
423
            revbranch = branch
341
424
        try:
342
 
            if revid == 'current:':
 
425
            if revid == CURRENT_REVISION:
343
426
                tree = revbranch.basis_tree()
344
427
            else:
345
428
                tree = revbranch.repository.revision_tree(revid)
358
441
        if self._history:
359
442
            return self.string_rev(self._history[0][1], self._history[0][0])
360
443
        else:
361
 
            return self.string_rev(None, 'null:')
 
444
            return self.string_rev(None, NULL_REVISION)
362
445
 
363
446
    def get_youngest_rev(self):
364
 
        if self._history is None:
365
 
            self._history = self._repo_history()
366
 
        if self._history:
367
 
            return self.string_rev(self._history[-1][1], self._history[-1][0])
368
 
        else:
369
 
            return self.string_rev(None, 'current:')
 
447
        # XXX: Find a better solution
 
448
        # The youngest revision may not be present in all branches, so if we
 
449
        # use current, then it maps out correctly.
 
450
        return self.string_rev(None, CURRENT_REVISION)
370
451
 
371
452
    def _repo_history(self):
372
453
        revisions = {}
390
471
 
391
472
    def previous_rev(self, rev):
392
473
        branch, revid = self._parse_rev(rev)
393
 
        if revid == 'null:':
 
474
        if revid == NULL_REVISION:
394
475
            return None
395
476
        if self._previous is None:
396
477
            if self._history is None:
403
484
                if last is not None:
404
485
                    self._previous[last] = (branch, rev)
405
486
                last = rev
406
 
        if revid == 'current:':
 
487
        if revid == CURRENT_REVISION:
407
488
            return self.string_rev(self._history[-1][1], self._history[-1][0])
408
489
        try:
409
490
            return self.string_rev(*self._previous[revid])
410
491
        except KeyError:
411
 
            return 'null:'
 
492
            return NULL_REVISION 
412
493
 
413
494
    def next_rev(self, rev, path=''):
414
495
        # TODO path is ignored.
415
496
        branch, revid = self._parse_rev(rev)
416
 
        if revid == 'current:':
 
497
        if revid == CURRENT_REVISION:
417
498
            return None
418
 
        if revid == 'null:':
419
 
            return 'current:'
 
499
        if revid == NULL_REVISION:
 
500
            return CURRENT_REVISION
420
501
        if branch is None:
421
502
            ancestry = self.repo.get_ancestry(self.branch.last_revision())
422
503
        else:
438
519
            return False
439
520
        branch1, rrev1 = self._parse_rev(rev1)
440
521
        branch2, rrev2 = self._parse_rev(rev2)
441
 
        if rrev2 == 'current:':
 
522
        if rrev2 == CURRENT_REVISION:
442
523
            return False
443
524
        first_before_second = rrev1 in branch2.repository.get_ancestry(rrev2)
444
525
        second_before_first = rrev2 in branch1.repository.get_ancestry(rrev1)
450
531
        if second_before_first:
451
532
            return False
452
533
        # Bah, unrelated revisions. Fall back to comparing timestamps.
453
 
        return (self.repo.get_revision(rrev1).timestamp <
454
 
                self.repo.get_revision(rrev2).timestamp)
 
534
        return (branch1.repository.get_revision(rrev1).timestamp <
 
535
                branch2.repository.get_revision(rrev2).timestamp)
455
536
 
456
537
    # XXX what is get_youngest_rev_in_cache doing in here
457
538
 
479
560
        """
480
561
        if rev is None:
481
562
            branch = None
482
 
            revid = 'current:'
 
563
            revid = CURRENT_REVISION
483
564
        else:
484
565
            branch, revid = self._parse_rev(rev)
485
566
        if branch is not None:
531
612
        new_branch, new_revid = self._parse_rev(new_rev)
532
613
        old_tree = old_branch.repository.revision_tree(old_revid)
533
614
        new_tree = new_branch.repository.revision_tree(new_revid)
534
 
        delta = new_tree.changes_from(old_tree)
 
615
        prefix = self.branch_path(new_branch)
 
616
        subdir = new_path[len(prefix)+1:]
 
617
        delta = new_tree.changes_from(old_tree, specific_files=[subdir])
535
618
        for path, file_id, kind in delta.added:
 
619
            path = osutils.pathjoin(prefix, path)
536
620
            entry = new_tree.inventory[file_id]
537
621
            node = NODE_MAP[kind](self, new_branch, new_tree, entry, path)
538
 
            cur_path = new_tree.id2path(file_id)
539
 
            node._history_cache[(new_revid, old_revid, file_id)] = \
540
 
                cur_path, new_rev, versioncontrol.Changeset.ADD
541
622
            yield None, node, node.kind, versioncontrol.Changeset.ADD
542
623
        for path, file_id, kind in delta.removed:
 
624
            path = osutils.pathjoin(prefix, path)
543
625
            entry = old_tree.inventory[file_id]
544
626
            node = NODE_MAP[kind](self, old_branch, old_tree, entry, path)
545
627
            yield node, None, node.kind, versioncontrol.Changeset.DELETE
546
628
        for oldpath, newpath, file_id, kind, textmod, metamod in delta.renamed:
 
629
            oldpath = osutils.pathjoin(prefix, oldpath)
 
630
            newpath = osutils.pathjoin(prefix, newpath)
547
631
            oldnode = NODE_MAP[kind](self, old_branch, old_tree,
548
632
                                     old_tree.inventory[file_id], oldpath)
549
633
            newnode = NODE_MAP[kind](self, new_branch, new_tree,
555
639
            yield oldnode, newnode, oldnode.kind, versioncontrol.Changeset.MOVE
556
640
        for path, file_id, kind, textmod, metamod in delta.modified:
557
641
            # Bzr won't report a changed path as a rename but trac wants that.
558
 
            oldpath = old_tree.id2path(file_id)
 
642
            path = osutils.pathjoin(prefix, path)
 
643
            oldpath = osutils.pathjoin(prefix, old_tree.id2path(file_id))
559
644
            oldnode = NODE_MAP[kind](self, old_branch, old_tree,
560
645
                                     old_tree.inventory[file_id], oldpath)
561
646
            newnode = NODE_MAP[kind](self, new_branch, new_tree,
568
653
                action = versioncontrol.Changeset.MOVE
569
654
            else:
570
655
                action = versioncontrol.Changeset.EDIT
571
 
            cur_path = new_tree.id2path(file_id)
572
 
            newnode._history_cache[(new_revid, old_revid, file_id)] = \
573
 
                cur_path, new_rev, action
574
656
            yield oldnode, newnode, oldnode.kind, action
575
657
 
576
658
    def dotted_revno(self, branch, revid):
582
664
            self._branch_cache[branch_key] = BranchCache(self, branch)
583
665
        return self._branch_cache[branch_key]
584
666
 
585
 
    def sorted_revision_history(self, branch):
586
 
        return self.get_branch_cache(branch).sorted_revision_history()
587
 
 
588
667
    def sync(self, rev_callback=None):
589
668
        """Dummy to satisfy interface requirements"""
590
669
        # XXX should we be dumping in-mem caches?  Seems unlikely.
591
 
        self.log = None
592
670
        pass
593
671
        
594
672
 
598
676
 
599
677
class UnversionedDirNode(BzrNode):
600
678
    def __init__(self, bzr_repo, path):
601
 
        rev_string = urllib.quote('current:')
 
679
        rev_string = urllib.quote(CURRENT_REVISION)
602
680
        BzrNode.__init__(self, path, rev_string, versioncontrol.Node.DIRECTORY)
603
681
        self.transport = bzr_repo.root_transport.clone(path)
604
682
        self.bzr_repo = bzr_repo
644
722
        return 'application/octet-stream'
645
723
 
646
724
    def get_history(self, limit=None):
647
 
        return [(self.path, 'current%3A', 'add')]
648
 
 
649
 
 
650
 
def sorted_revision_history(branch, generate_revno=False):
651
 
    history = branch.revision_history()
652
 
    graph = branch.repository.get_revision_graph(history[-1])
653
 
    return tsort.merge_sort(graph, history[-1], generate_revno=generate_revno)
 
725
        return [(self.path, BzrRepository._escape(CURRENT_REVISION), 'add')]
654
726
 
655
727
 
656
728
class BzrVersionedNode(BzrNode):
657
729
 
658
 
    _history_cache = {}
659
730
    _diff_map = {
660
731
        'modified': versioncontrol.Changeset.EDIT,
661
732
        'unchanged': versioncontrol.Changeset.EDIT,
688
759
            result['executable'] = 'True'
689
760
        return result
690
761
 
691
 
    def _merging_history(self):
692
 
        """Iterate through history revisions that merged changes to this node
693
 
        
694
 
        This includes all revisions in which the revision_id changed.
695
 
        It may also include a few revisions in which the revision_id did not
696
 
        change, if the modification was subsequently undone.
697
 
        """
698
 
        weave = self.tree._get_weave(self.entry.file_id)
699
 
        file_ancestry = weave.get_ancestry(self.entry.revision)
700
 
        # Can't use None here, because it's a legitimate revision id.
701
 
        last_yielded = 'bogus:'
702
 
        for num, revision_id, depth, revno, eom in \
703
 
            self.bzr_repo.sorted_revision_history(self.branch):
704
 
            if depth == 0:
705
 
                last_mainline = revision_id
706
 
            if last_mainline == last_yielded:
707
 
                continue
708
 
            if revision_id in file_ancestry:
709
 
                yield last_mainline
710
 
                last_yielded = last_mainline
711
 
        yield None
712
 
 
713
 
    def get_history(self, limit=None):
714
 
        """Backward history.
715
 
 
716
 
        yields (path, revid, chg) tuples.
717
 
 
718
 
        path is the path to this entry. 
719
 
        
720
 
        revid is the revid string.  It is the revision in which the change
721
 
        was applied to the branch, not necessarily the revision that originated
722
 
        the change.  In SVN terms, it is a changeset, not a file revision.
723
 
        
724
 
        chg is a Changeset.ACTION thing.
725
 
 
726
 
        First thing should be for the current revision.
727
 
 
728
 
        limit is an int cap on how many entries to return.
729
 
        """
730
 
        history_iter = self._get_history()
731
 
        if limit is None:
732
 
            return history_iter
733
 
        else:
734
 
            return (y for x, y in izip(range(limit), history_iter))
735
 
 
736
 
    def _get_history(self, limit=None):
737
 
        file_id = self.entry.file_id
738
 
        revision = None
739
 
        history = list(self._merging_history())
740
 
        cache = self.bzr_repo.get_branch_cache(self.branch)
741
 
        trees = cache.revision_trees(history)
742
 
        for prev_tree in trees:
743
 
            previous_revision = prev_tree.get_revision_id()
744
 
            try:
745
 
                prev_file_revision = prev_tree.inventory[file_id].revision
746
 
            except errors.NoSuchId:
747
 
                prev_file_revision = None
748
 
            if (revision is not None and 
749
 
                prev_file_revision != file_revision):
750
 
                path, rev_str, chg = \
751
 
                    self.get_change(revision, previous_revision, file_id)
752
 
                branch_revision = self.bzr_repo.string_rev(self.branch, 
753
 
                                                           revision)
754
 
                yield (osutils.pathjoin(self.root_path, path), 
755
 
                       branch_revision, chg)
756
 
            if prev_file_revision is None:
757
 
                break
758
 
            revision = previous_revision
759
 
            file_revision = prev_file_revision
760
 
 
761
 
    def get_change(self, revision, previous_revision, file_id):
762
 
        key = (revision, previous_revision, file_id)
763
 
        if key not in self._history_cache or False:
764
 
            self._history_cache[key] = self.calculate_history(revision, 
765
 
                previous_revision, file_id)
766
 
        return self._history_cache[key]
767
 
 
768
 
    def calculate_history(self, revision, previous_revision, file_id):
769
 
        cache = self.bzr_repo.get_branch_cache(self.branch)
770
 
        tree = cache.revision_tree(revision)
771
 
        current_entry = tree.inventory[file_id]
772
 
        current_path = tree.id2path(file_id)
773
 
        if previous_revision not in (None, 'null:'):
774
 
            previous_tree = cache.revision_tree(previous_revision)
775
 
            previous_entry = previous_tree.inventory[file_id]
776
 
        else:
777
 
            previous_entry = None
778
 
        # We should only get revisions in the ancestry for which
779
 
        # we exist, so this should succeed..
780
 
        return self.compare_entries(current_path, current_entry, 
781
 
                                    previous_entry)
782
 
 
783
 
    def compare_entries(self, current_path, current_entry, previous_entry):
784
 
        diff = current_entry.describe_change(previous_entry, current_entry)
785
 
        rev = self.bzr_repo.string_rev(self.branch, current_entry.revision)
786
 
        try:
787
 
            return current_path, rev, self._diff_map[diff]
788
 
        except KeyError:
789
 
            raise Exception('unknown describe_change %r' % (diff,))
790
 
 
791
 
    def get_last_modified(self):
792
 
        return self.tree.get_file_mtime(self.entry.file_id)
793
 
 
794
 
 
795
 
class BzrDirNode(BzrVersionedNode):
796
 
 
797
 
    isdir = True
798
 
    isfile = False
799
 
    kind = versioncontrol.Node.DIRECTORY
800
 
 
801
 
    def __init__(self, bzr_repo, branch, revisiontree, entry, path,
802
 
                 revcache=None):
803
 
        BzrVersionedNode.__init__(self, bzr_repo, branch, revisiontree, entry, 
804
 
                                  path)
805
 
        if revcache is None:
806
 
            ancestry = self.repo.get_ancestry(revisiontree.get_revision_id())
807
 
            ancestry.reverse()
808
 
            self.revcache = {}
809
 
            best = self._get_cache(self.revcache, ancestry, entry)
810
 
            self._orig_rev = ancestry[best]
811
 
            self.rev = bzr_repo.string_rev(self.branch, (ancestry[best]))
812
 
        else:
813
 
            self.revcache = revcache
814
 
            self._orig_rev = revcache[entry.file_id]
815
 
            self.rev = bzr_repo.string_rev(self.branch, self._orig_rev)
816
 
 
817
 
    def __repr__(self):
818
 
        return 'BzrDirNode(path=%r, relpath=%r)' % (self.path, self.entry.name)
819
 
 
820
 
    @classmethod
821
 
    def _get_cache(cls, cache, ancestry, entry, ancestry_idx=None):
822
 
        """Populate a file_id <-> revision_id mapping.
823
 
        
824
 
        This mapping is different from InventoryEntry.revision, but only for
825
 
        directories.  In this scheme, directories are considered modified
826
 
        if their contents are modified.
827
 
 
828
 
        The revision ids are not guaranteed to be in the mainline revision
829
 
        history.
830
 
 
831
 
        cache: The cache to populate
832
 
        ancestry: A topologically-sorted list of revisions, with more recent
833
 
            revisions having lower indexes.
834
 
        entry: The InventoryEntry to start at
835
 
        ancestry_idx: A mapping of revision_id <-> ancestry index.
836
 
        """
837
 
        if ancestry_idx is None:
838
 
            ancestry_idx = dict((r, n) for n, r in enumerate(ancestry))
839
 
        # best ~= most recent revision to modify a child of this directory
840
 
        best = ancestry_idx[entry.revision]
841
 
        for child in entry.children.itervalues():
842
 
            if child.kind == 'directory':
843
 
                index = cls._get_cache(cache, ancestry, child, ancestry_idx)
844
 
                cache[child.file_id] = ancestry[index]
845
 
            else:
846
 
                index = ancestry_idx[child.revision]
847
 
            best = min(best, index)
848
 
        return best
849
 
 
850
 
    def get_content(self):
851
 
        """Return a file-like (read(length)) for a file, None for a dir."""
852
 
        return None
853
 
 
854
 
    def get_entries(self):
855
 
        """Yield child Nodes if a dir, return None if a file."""
856
 
        for name, entry in self.entry.children.iteritems():
857
 
            childpath = '/'.join((self.path, name))
858
 
            klass = NODE_MAP[entry.kind]
859
 
            if klass is BzrDirNode:
860
 
                yield klass(self.bzr_repo, self.branch, self.tree, entry,
861
 
                            childpath, self.revcache)
862
 
            else:
863
 
                yield klass(self.bzr_repo, self.branch, self.tree, entry,
864
 
                            childpath)
865
 
 
866
 
    def get_content_length(self):
867
 
        return None
868
 
 
869
 
    def get_content_type(self):
870
 
        return None
871
 
 
872
 
    def _get_revision_history(self, limit=None):
873
 
        history = self.branch.revision_history()
874
 
        first = history[0]
875
 
        if limit is not None:
876
 
            history = history[-limit:]
877
 
        self.bzr_repo.get_branch_cache(self.branch).cache_revisions(history)
878
 
        for rev_id in reversed(history):
879
 
            if rev_id == first:
880
 
                operation = versioncontrol.Changeset.ADD
881
 
            else:
882
 
                operation = versioncontrol.Changeset.EDIT
883
 
            yield (self.path, 
884
 
                   self.bzr_repo.string_rev(self.branch, rev_id),
885
 
                   operation)
886
 
 
887
762
    def get_history(self, limit=None):
888
763
        """Backward history.
889
764
 
903
778
        """
904
779
        current_entry = self.entry
905
780
        file_id = current_entry.file_id
906
 
        if current_entry.parent_id == None:
907
 
            for r in self._get_revision_history(limit):
908
 
                yield r
909
 
            return
910
781
            
911
782
        count = 0
912
783
        # We need the rev we were created with, not the rev the entry
913
784
        # specifies (our contents may have changed between that rev
914
785
        # and our own current rev).
915
 
        current_revid = self._orig_rev
916
 
 
917
 
        if self.branch is not None:
918
 
            history = self.branch.revision_history()
919
 
        else:
920
 
            history = []
921
 
        if current_revid == 'current:':
922
 
            current_revid = history[-1]
923
 
        # If the current_revid we start from is in the branch history,
924
 
        # limit our view to just the history, not the full ancestry.
925
 
        try:
926
 
            index = history.index(current_revid)
927
 
        except ValueError:
928
 
            ancestry = self.repo.get_ancestry(current_revid)
929
 
            # The last entry is this rev, skip it.
930
 
            ancestry.pop()
931
 
            ancestry.reverse()
932
 
            # The last entry is None, skip it.
933
 
            ancestry.pop()
934
 
        else:
935
 
            ancestry = ['null:'] + history[:index]
936
 
            ancestry.reverse()
 
786
        current_revid = self.get_content_revision()
 
787
        if current_revid == CURRENT_REVISION:
 
788
            current_revid = self.branch.last_revision()
 
789
 
 
790
        # Aways use a linear view of our ancestry.
 
791
        repo = self.branch.repository
 
792
        ancestry = repo.iter_reverse_revision_history(current_revid)
 
793
        ancestry = iter(ancestry)
 
794
        # ancestry now iterates from current_revid down to revno 1
 
795
 
 
796
        # Optimize the special case that this is the root of the
 
797
        # branch. In this case, all revisions from the ancestry apply
 
798
        # to us, and we can simply return them without further checks.
 
799
        if self.entry.parent_id is None:
 
800
            for rev_id in ancestry:
 
801
                yield (self.path, 
 
802
                       self.bzr_repo.string_rev(self.branch, rev_id),
 
803
                       versioncontrol.Changeset.EDIT)
 
804
            yield (self.path, 
 
805
                   self.bzr_repo.string_rev(self.branch, NULL_REVISION),
 
806
                   versioncontrol.Changeset.ADD)
 
807
            return
 
808
 
937
809
        # Load a bunch of trees in one go. We do not know how many we
938
810
        # need: we may end up skipping some trees because they do not
939
811
        # change us.
940
812
        chunksize = limit or 100
 
813
        chunk = []
 
814
        ancestry.next() # drop current_revno, we already know about that
941
815
        current_tree = self.tree
942
816
        current_path = current_tree.id2path(file_id)
943
817
        path_prefix = self.path[:-len(current_path)]
944
 
        while ancestry:
945
 
            chunk, ancestry = ancestry[:chunksize], ancestry[chunksize:]
 
818
        while True:
 
819
            chunk[:] = []
 
820
            try:
 
821
                for i in range(chunksize):
 
822
                    chunk.append(ancestry.next())
 
823
            except StopIteration:
 
824
                if not chunk:
 
825
                    yield (path_prefix+current_path,
 
826
                           self.bzr_repo.string_rev(self.branch, 
 
827
                                                    current_revid),
 
828
                           versioncontrol.Changeset.ADD)
 
829
                    return
946
830
            cache = self.bzr_repo.get_branch_cache(self.branch)
947
831
            for previous_revid, previous_tree in izip(
948
832
                chunk, cache.revision_trees(chunk)):
989
873
                current_revid = previous_revid
990
874
                current_tree = previous_tree
991
875
 
992
 
    def get_previous(self):
993
 
        """Equivalent to i=iter(get_history(2));i.next();return i.next().
994
 
 
995
 
        The default implementation does essentially that, but we specialcase
996
 
        it because we can skip the loading of all the trees.
 
876
    def get_content_revision(self):
 
877
        """For non-directory nodes, the content was last changed here."""
 
878
        return self.entry.revision
 
879
 
 
880
    def get_last_modified(self):
 
881
        return self.tree.get_file_mtime(self.entry.file_id)
 
882
 
 
883
 
 
884
class BzrDirNode(BzrVersionedNode):
 
885
 
 
886
    isdir = True
 
887
    isfile = False
 
888
    kind = versioncontrol.Node.DIRECTORY
 
889
 
 
890
    def __init__(self, bzr_repo, branch, revisiontree, entry, path,
 
891
                 revcache=None):
 
892
        BzrVersionedNode.__init__(self, bzr_repo, branch, revisiontree, entry, 
 
893
                                  path)
 
894
        if revcache is None:
 
895
            revcache = _FileToRevisionCache(bzr_repo, branch, revisiontree, entry)
 
896
        self.revcache = revcache
 
897
        self.rev = self.bzr_repo.string_rev(self.branch,
 
898
                                            self.get_content_revision())
 
899
 
 
900
    def get_content_revision(self):
 
901
        """Determine the most recent change to the directory or its children."""
 
902
        if self.entry.parent_id is None:
 
903
            return self.tree.get_revision_id()
 
904
        return self.revcache[self.entry]
 
905
 
 
906
    def __repr__(self):
 
907
        return 'BzrDirNode(path=%r, relpath=%r)' % (self.path, self.entry.name)
 
908
 
 
909
    @classmethod
 
910
    def _get_cache(cls, cache, ancestry, entry, ancestry_idx=None):
 
911
        """Populate a file_id <-> revision_id mapping.
 
912
        
 
913
        This mapping is different from InventoryEntry.revision, but only for
 
914
        directories.  In this scheme, directories are considered modified
 
915
        if their contents are modified.
 
916
 
 
917
        The revision ids are not guaranteed to be in the mainline revision
 
918
        history.
 
919
 
 
920
        cache: The cache to populate
 
921
        ancestry: A topologically-sorted list of revisions, with more recent
 
922
            revisions having lower indexes.
 
923
        entry: The InventoryEntry to start at
 
924
        ancestry_idx: A mapping of revision_id <-> ancestry index.
997
925
        """
998
 
        # Special case: if this is the root node it (well, its
999
 
        # contents) change every revision.
1000
 
        if not self.tree.id2path(self.entry.file_id):
1001
 
            return self.path, self.rev, versioncontrol.Changeset.EDIT
1002
 
        if self._orig_rev != self.entry.revision:
1003
 
            # The last change did not affect this dir directly, it changed
1004
 
            # our contents.
1005
 
            return self.path, self.rev, versioncontrol.Changeset.EDIT
1006
 
        # We were affected directly. Get a delta to figure out how.
1007
 
        delta = self.repo.get_revision_delta(self._orig_rev)
1008
 
        for path, file_id, kind in delta.added:
1009
 
            if file_id == self.entry.file_id:
1010
 
                return path, self.rev, versioncontrol.Changeset.ADD
1011
 
        for oldpath, newpath, file_id, kind, textmod, metamod in delta.renamed:
1012
 
            if file_id == self.entry.file_id:
1013
 
                return newpath, self.rev, versioncontrol.Changeset.MOVE
1014
 
        # We were removed (which does not make any sense,
1015
 
        # the tree we were constructed from is newer and has us)
1016
 
        raise core.TracError('should not get here, %r %r %r' %
1017
 
                             (self.entry, delta, self._orig_rev))
 
926
        if ancestry_idx is None:
 
927
            ancestry_idx = dict((r, n) for n, r in enumerate(ancestry))
 
928
        # best ~= most recent revision to modify a child of this directory
 
929
        best = ancestry_idx[entry.revision]
 
930
        for child in entry.children.itervalues():
 
931
            if child.kind == 'directory':
 
932
                index = cls._get_cache(cache, ancestry, child, ancestry_idx)
 
933
                cache[child.file_id] = ancestry[index]
 
934
            else:
 
935
                index = ancestry_idx[child.revision]
 
936
            best = min(best, index)
 
937
        return best
 
938
 
 
939
    def get_content(self):
 
940
        """Return a file-like (read(length)) for a file, None for a dir."""
 
941
        return None
 
942
 
 
943
    def get_entries(self):
 
944
        """Yield child Nodes if a dir, return None if a file."""
 
945
        for name, entry in self.entry.children.iteritems():
 
946
            childpath = '/'.join((self.path, name))
 
947
            klass = NODE_MAP[entry.kind]
 
948
            if klass is BzrDirNode:
 
949
                yield klass(self.bzr_repo, self.branch, self.tree, entry,
 
950
                            childpath, self.revcache)
 
951
            else:
 
952
                yield klass(self.bzr_repo, self.branch, self.tree, entry,
 
953
                            childpath)
 
954
 
 
955
    def get_content_length(self):
 
956
        return None
 
957
 
 
958
    def get_content_type(self):
 
959
        return None
1018
960
 
1019
961
 
1020
962
class BzrFileNode(BzrVersionedNode):
1086
1028
        assert isinstance(revid, str), "revid is %r" % type(revid)
1087
1029
        self.log = log
1088
1030
        self.bzr_repo = bzr_repo
 
1031
        self.branch = branch
1089
1032
        
1090
 
        # Check trac version.  trac 0.11 uses datetime objects for
1091
 
        # timestamp information.
1092
 
        if int(trac.__version__[2:4]) < 11:
1093
 
            use_datetime = False
1094
 
        else:
1095
 
            use_datetime = True
1096
 
                
1097
 
        if branch is None:                
1098
 
            if revid in ('current:', 'null:'):
1099
 
                self.revision = revision.Revision(revid, committer='', 
1100
 
                                                  message='', timezone='')
1101
 
                revidstr = bzr_repo.string_rev(branch, revid)
1102
 
                if use_datetime:
1103
 
                    versioncontrol.Changeset.__init__(self, revidstr,
1104
 
                                             '', '', datetime.datetime.now(utc))
1105
 
                else:
1106
 
                    versioncontrol.Changeset.__init__(self, revidstr,
1107
 
                                                      '', '', time.time())
1108
 
            else:
1109
 
                assert isinstance(revid, str)
1110
 
                raise errors.NoSuchRevision(None, revid)
1111
 
        else:
1112
 
            self.revision = bzr_repo.get_branch_cache(branch).get_revision(revid)
1113
 
            if use_datetime:
1114
 
                timestamp = datetime.datetime.\
1115
 
                            fromtimestamp(self.revision.timestamp, utc)
1116
 
                versioncontrol.Changeset.__init__(self,
1117
 
                                                  bzr_repo.string_rev(
1118
 
                                                  branch, revid),
1119
 
                                                  self.revision.message,
1120
 
                                                  self.revision.committer,
1121
 
                                                  timestamp)
1122
 
            else:
1123
 
                versioncontrol.Changeset.__init__(self,
1124
 
                                                  bzr_repo.string_rev(
1125
 
                                                  branch, revid),
1126
 
                                                  self.revision.message,
1127
 
                                                  self.revision.committer,
1128
 
                                                  self.revision.timestamp)
1129
 
        self.branch = branch
 
1033
        if revid == CURRENT_REVISION and branch is not None:
 
1034
            revid = branch.revision_history()
 
1035
        if revid in (CURRENT_REVISION, NULL_REVISION):
 
1036
            self.revision = revision.Revision(revid, committer='', 
 
1037
                                              message='', timezone='')
 
1038
            revidstr = bzr_repo.string_rev(branch, revid)
 
1039
            versioncontrol.Changeset.__init__(self, revidstr,
 
1040
                                              '', '', trac_timestamp(time.time()))
 
1041
            return
 
1042
        if branch is None:
 
1043
            assert isinstance(revid, str)
 
1044
            raise errors.NoSuchRevision(None, revid)
 
1045
 
 
1046
        self.revision = bzr_repo.get_branch_cache(branch).get_revision(revid)
 
1047
        authors = ";".join(self.revision.get_apparent_authors())
 
1048
        versioncontrol.Changeset.__init__(self,
 
1049
                                          bzr_repo.string_rev(
 
1050
                                              branch, revid),
 
1051
                                          self.revision.message,
 
1052
                                          authors,
 
1053
                                          trac_timestamp(self.revision.timestamp))
1130
1054
 
1131
1055
    def __repr__(self):
1132
1056
        return 'BzrChangeset(%r)' % (self.revision.revision_id)
1133
1057
 
1134
1058
    def get_properties(self):
1135
1059
        """Return an iterator of (name, value, is wikitext, html class)."""
1136
 
        result = {}
 
1060
        if trac_version >= (0,11):
 
1061
            result = {}
 
1062
            def add_text(name, value):
 
1063
                result[name] = value
 
1064
            def add_parents(parents):
 
1065
                result['parents'] = '\n'.join(parents)
 
1066
        else:
 
1067
            result = []
 
1068
            def add_text(name, value):
 
1069
                result.append((name, value, False, ''))
 
1070
            def add_parents(parents):
 
1071
                for name, link in [('parent trees', ' * source:@%s'),
 
1072
                                   ('changesets', ' * [changeset:%s]')]:
 
1073
                    wikitext = '\n'.join(link % parent for parent in parents)
 
1074
                    result.append((name, wikitext, True, ''))
 
1075
        add_text('revision id', self.revision.revision_id)
1137
1076
        for name, value in self.revision.properties.iteritems():
1138
 
            result[name] = value
 
1077
            add_text(name, value)
1139
1078
        if len(self.revision.parent_ids) > 1:
1140
 
            for name, link in [('parent trees', ' * source:@%s'),
1141
 
                               ('changesets', ' * [changeset:%s]')]:
1142
 
                result[name] = '\n'.join(
1143
 
                    link % (self.bzr_repo.string_rev(self.branch, parent),)
1144
 
                    for parent in self.revision.parent_ids)
 
1079
            add_parents([self.bzr_repo.string_rev(self.branch, parent)
 
1080
                         for parent in self.revision.parent_ids])
1145
1081
        return result
1146
1082
 
1147
1083
    def get_changes(self):
1153
1089
        base_path and base_rev are the location and revision of the file
1154
1090
        before "ours".
1155
1091
        """
1156
 
        if self.revision.revision_id in ('current:', 'null:'):
 
1092
        if self.revision.revision_id in (CURRENT_REVISION, NULL_REVISION):
1157
1093
            return
1158
1094
        branchpath = osutils.relpath(
1159
1095
            osutils.normalizepath(self.bzr_repo.root_transport.base),
1166
1102
            yield (path, kind, change, base_path, base_rev)
1167
1103
 
1168
1104
    def _get_changes(self):
1169
 
        if self.revision.revision_id in ('current:', 'null:'):
 
1105
        if self.revision.revision_id in (CURRENT_REVISION, NULL_REVISION):
1170
1106
            return
1171
1107
        this = self.branch.repository.revision_tree(self.revision.revision_id)
1172
1108
        parents = self.revision.parent_ids
1212
1148
                   self.bzr_repo.string_rev(self.branch, parent_revid))
1213
1149
 
1214
1150
 
1215
 
def getattribute(self, attr):
1216
 
    """If a callable is requested log the call."""
1217
 
    obj = object.__getattribute__(self, attr)
1218
 
    if not callable(obj):
1219
 
        return obj
1220
 
    try:
1221
 
        log = object.__getattribute__(self, 'log')
1222
 
    except AttributeError:
1223
 
        return obj
1224
 
    if log is None:
1225
 
        return obj
1226
 
    else:
1227
 
        def _wrap(*args, **kwargs):
1228
 
            arg_list = [repr(a) for a in args] + \
1229
 
                ['%s=%r' % i for i in kwargs.iteritems() ]
1230
 
            call_str = '%s.%s(%s)' % \
1231
 
                (self, attr, ', '.join(arg_list))
1232
 
            if len(call_str) > 200:
1233
 
                call_str = '%s...' % call_str[:200]
1234
 
            log.debug('CALL %s' % call_str)
1235
 
            try:
1236
 
                result = obj(*args, **kwargs)
1237
 
            except Exception, e:
1238
 
                log.debug('FAILURE of %s: %s' % (call_str, e) )
1239
 
                raise
1240
 
            str_result = str(result)
1241
 
            if len(str_result) > 200:
1242
 
                str_result = '%s...' % str_result[:200]
1243
 
            log.debug('RESULT of %s: %s' % (call_str, str_result))
1244
 
            return result
1245
 
        return _wrap
1246
 
 
1247
 
 
1248
1151
def containing_branch(transport, path):
1249
1152
    child_transport = transport.clone(path)
1250
1153
    my_bzrdir, relpath = \
1257
1160
    def __init__(self, bzr_repo, branch):
1258
1161
        self.bzr_repo = bzr_repo
1259
1162
        self.branch = branch
1260
 
        self._sorted_revision_history = None
1261
 
        self._dotted_revno = None
1262
1163
        self._revno_revid = None
1263
1164
 
1264
 
    def sorted_revision_history(self):
1265
 
        if self._sorted_revision_history is None:
1266
 
            self._sorted_revision_history = \
1267
 
                sorted_revision_history(self.branch, generate_revno=True)
1268
 
        return self._sorted_revision_history
1269
 
 
1270
 
    def _populate_dotted_maps(self):
1271
 
        if self._dotted_revno is None:
1272
 
            self._dotted_revno = {}
1273
 
            self._revno_revid = {}
1274
 
            for s, revision_id, m, revno, e in \
1275
 
                self.sorted_revision_history():
1276
 
                    dotted = '.'.join([str(s) for s in revno])
1277
 
                    self._dotted_revno[revision_id] = dotted
1278
 
                    self._revno_revid[dotted] = revision_id
1279
 
 
1280
1165
    def dotted_revno(self, revid):
1281
 
        self._populate_dotted_maps()
1282
 
        return self._dotted_revno.get(revid)
 
1166
        try:
 
1167
            revno = self.branch.revision_id_to_dotted_revno(revid)
 
1168
            dotted = '.'.join([str(s) for s in revno])
 
1169
            return dotted
 
1170
        except errors.NoSuchRevision:
 
1171
            return None
1283
1172
 
1284
1173
    def revid_from_dotted(self, dotted_revno):
1285
 
        self._populate_dotted_maps()
1286
 
        return self._revno_revid.get(dotted_revno)
 
1174
        try:
 
1175
            revno = tuple([int(s) for s in dotted_revno.split('.')])
 
1176
            revid = self.branch.dotted_revno_to_revision_id(revno)
 
1177
            return revid
 
1178
        except errors.NoSuchRevision:
 
1179
            return None
1287
1180
 
1288
1181
    def revision_tree(self, revision_id):
1289
1182
        if revision_id not in self.bzr_repo._tree_cache:
1290
1183
            self.bzr_repo._tree_cache[revision_id] = \
1291
1184
                self.branch.repository.revision_tree(revision_id)
1292
 
            if revision_id == 'null:':
 
1185
            if revision_id == NULL_REVISION:
1293
1186
                self.bzr_repo._tree_cache[None] = \
1294
 
                    self.bzr_repo._tree_cache['null:']
 
1187
                    self.bzr_repo._tree_cache[NULL_REVISION]
1295
1188
        return self.bzr_repo._tree_cache[revision_id]
1296
1189
 
1297
1190
    def revision_trees(self, revision_ids):
1298
 
        if None in revision_ids or 'null:' in revision_ids:
1299
 
            self.revision_tree('null:')
 
1191
        if None in revision_ids or NULL_REVISION in revision_ids:
 
1192
            self.revision_tree(NULL_REVISION)
1300
1193
        missing = [r for r in revision_ids if r not in 
1301
1194
                   self.bzr_repo._tree_cache]
1302
1195
        if len(missing) > 0:
1323
1216
        except KeyError:
1324
1217
            self.cache_revisions([revision_id])
1325
1218
        return self.bzr_repo._revision_cache[revision_id]
 
1219
 
 
1220
 
 
1221
class _FileToRevisionCache(object):
 
1222
    """Map from file_id to revision_id.
 
1223
 
 
1224
    This is used to determine the last modification to a directory.
 
1225
    """
 
1226
 
 
1227
    def __init__(self, bzr_repo, branch, revisiontree, root):
 
1228
        self.bzr_repo = bzr_repo
 
1229
        self.branch = branch
 
1230
        self.revisiontree = revisiontree
 
1231
        self.root = root
 
1232
        self.revcache = None
 
1233
 
 
1234
    def __getitem__(self, entry):
 
1235
        """Lazily fill cache only when needed."""
 
1236
        if self.revcache is None:
 
1237
            revid = self.revisiontree.get_revision_id()
 
1238
            ancestry = self.branch.repository.get_ancestry(revid)
 
1239
            ancestry.reverse()
 
1240
            ancestry_idx = dict((r, n) for n, r in enumerate(ancestry))
 
1241
            self.revcache = {}
 
1242
            best = self._fill_cache(ancestry, ancestry_idx, self.root)
 
1243
        return self.revcache[entry.file_id]
 
1244
 
 
1245
    def _fill_cache(self, ancestry, ancestry_idx, entry):
 
1246
        """Populate a file_id <-> revision_id mapping.
 
1247
        
 
1248
        This mapping is different from InventoryEntry.revision, but only for
 
1249
        directories.  In this scheme, directories are considered modified
 
1250
        if their contents are modified.
 
1251
 
 
1252
        The revision ids are not guaranteed to be in the mainline revision
 
1253
        history.
 
1254
 
 
1255
        ancestry: A topologically-sorted list of revisions, with more recent
 
1256
            revisions having lower indexes.
 
1257
        ancestry_idx: A mapping of revision_id <-> ancestry index.
 
1258
        entry: The InventoryEntry to start at
 
1259
        """
 
1260
        # best ~= most recent revision to modify a child of this directory
 
1261
        best = ancestry_idx[entry.revision]
 
1262
        for child in entry.children.itervalues():
 
1263
            if child.kind == 'directory':
 
1264
                index = self._fill_cache(ancestry, ancestry_idx, child)
 
1265
            else:
 
1266
                index = ancestry_idx[child.revision]
 
1267
            best = min(best, index)
 
1268
        self.revcache[entry.file_id] = ancestry[best]
 
1269
        return best
 
1270