~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/history.py

  • Committer: John Arbash Meinel
  • Date: 2011-02-10 00:43:37 UTC
  • mto: This revision was merged to the branch mainline in revision 426.
  • Revision ID: john@arbash-meinel.com-20110210004337-8rwedln8fgg4un16
Add a <noop> command to the RequestWorker.

This allows us to push the workers to stop immediately, in case
they are currently blocked waiting on another item in the queue.
This makes the test suite integration tests faster, but also
makes the script runner exit in a timely manner as well.

Also creating a trivial load_test script, just for example
purposes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
 
2
# Copyright (C) 2008, 2009 Canonical Ltd.
 
3
#                     (Authored by Martin Albisetti <argentina@gmail.com>)
2
4
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
 
5
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
 
6
# Copyright (C) 2005  Jake Edge <jake@edge2.net>
 
7
# Copyright (C) 2005  Matt Mackall <mpm@selenic.com>
3
8
#
4
9
# This program is free software; you can redistribute it and/or modify
5
10
# it under the terms of the GNU General Public License as published by
16
21
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
22
#
18
23
 
19
 
import cgi
 
24
#
 
25
# This file (and many of the web templates) contains work based on the
 
26
# "bazaar-webserve" project by Goffredo Baroncelli, which is in turn based
 
27
# on "hgweb" by Jake Edge and Matt Mackall.
 
28
#
 
29
 
 
30
 
 
31
import bisect
20
32
import datetime
21
33
import logging
22
 
import os
23
 
import posixpath
24
 
import shelve
 
34
import re
25
35
import textwrap
26
36
import threading
27
 
import time
28
 
from StringIO import StringIO
29
37
 
30
 
import bzrlib
31
 
import bzrlib.annotate
32
38
import bzrlib.branch
33
 
import bzrlib.diff
 
39
import bzrlib.delta
34
40
import bzrlib.errors
35
 
import bzrlib.textfile
36
 
import bzrlib.tsort
 
41
import bzrlib.foreign
 
42
import bzrlib.revision
37
43
 
 
44
from loggerhead import search
38
45
from loggerhead import util
39
 
 
40
 
log = logging.getLogger("loggerhead.controllers")
41
 
 
42
 
 
43
 
class History (object):
44
 
    
45
 
    def __init__(self):
46
 
        self._change_cache = None
47
 
        log.error('new history: %r' % (self,))
48
 
        self.log = log
49
 
    
50
 
    def __del__(self):
51
 
        if self._change_cache is not None:
52
 
            self._change_cache.close()
53
 
            self._change_cache_diffs.close()
54
 
            self._change_cache = None
55
 
            self._change_cache_diffs = None
56
 
 
57
 
    @classmethod
58
 
    def from_branch(cls, branch):
59
 
        z = time.time()
60
 
        self = cls()
 
46
from loggerhead.wholehistory import compute_whole_history_data
 
47
 
 
48
 
 
49
def is_branch(folder):
 
50
    try:
 
51
        bzrlib.branch.Branch.open(folder)
 
52
        return True
 
53
    except:
 
54
        return False
 
55
 
 
56
 
 
57
def clean_message(message):
 
58
    """Clean up a commit message and return it and a short (1-line) version.
 
59
 
 
60
    Commit messages that are long single lines are reflowed using the textwrap
 
61
    module (Robey, the original author of this code, apparently favored this
 
62
    style of message).
 
63
    """
 
64
    message = message.lstrip().splitlines()
 
65
 
 
66
    if len(message) == 1:
 
67
        message = textwrap.wrap(message[0])
 
68
 
 
69
    if len(message) == 0:
 
70
        # We can end up where when (a) the commit message was empty or (b)
 
71
        # when the message consisted entirely of whitespace, in which case
 
72
        # textwrap.wrap() returns an empty list.
 
73
        return [''], ''
 
74
 
 
75
    # Make short form of commit message.
 
76
    short_message = message[0]
 
77
    if len(short_message) > 60:
 
78
        short_message = short_message[:60] + '...'
 
79
 
 
80
    return message, short_message
 
81
 
 
82
 
 
83
def rich_filename(path, kind):
 
84
    if kind == 'directory':
 
85
        path += '/'
 
86
    if kind == 'symlink':
 
87
        path += '@'
 
88
    return path
 
89
 
 
90
 
 
91
class _RevListToTimestamps(object):
 
92
    """This takes a list of revisions, and allows you to bisect by date"""
 
93
 
 
94
    __slots__ = ['revid_list', 'repository']
 
95
 
 
96
    def __init__(self, revid_list, repository):
 
97
        self.revid_list = revid_list
 
98
        self.repository = repository
 
99
 
 
100
    def __getitem__(self, index):
 
101
        """Get the date of the index'd item"""
 
102
        return datetime.datetime.fromtimestamp(self.repository.get_revision(
 
103
                   self.revid_list[index]).timestamp)
 
104
 
 
105
    def __len__(self):
 
106
        return len(self.revid_list)
 
107
 
 
108
class FileChangeReporter(object):
 
109
 
 
110
    def __init__(self, old_inv, new_inv):
 
111
        self.added = []
 
112
        self.modified = []
 
113
        self.renamed = []
 
114
        self.removed = []
 
115
        self.text_changes = []
 
116
        self.old_inv = old_inv
 
117
        self.new_inv = new_inv
 
118
 
 
119
    def revid(self, inv, file_id):
 
120
        try:
 
121
            return inv[file_id].revision
 
122
        except bzrlib.errors.NoSuchId:
 
123
            return 'null:'
 
124
 
 
125
    def report(self, file_id, paths, versioned, renamed, modified,
 
126
               exe_change, kind):
 
127
        if modified not in ('unchanged', 'kind changed'):
 
128
            if versioned == 'removed':
 
129
                filename = rich_filename(paths[0], kind[0])
 
130
            else:
 
131
                filename = rich_filename(paths[1], kind[1])
 
132
            self.text_changes.append(util.Container(
 
133
                filename=filename, file_id=file_id,
 
134
                old_revision=self.revid(self.old_inv, file_id),
 
135
                new_revision=self.revid(self.new_inv, file_id)))
 
136
        if versioned == 'added':
 
137
            self.added.append(util.Container(
 
138
                filename=rich_filename(paths[1], kind),
 
139
                file_id=file_id, kind=kind[1]))
 
140
        elif versioned == 'removed':
 
141
            self.removed.append(util.Container(
 
142
                filename=rich_filename(paths[0], kind),
 
143
                file_id=file_id, kind=kind[0]))
 
144
        elif renamed:
 
145
            self.renamed.append(util.Container(
 
146
                old_filename=rich_filename(paths[0], kind[0]),
 
147
                new_filename=rich_filename(paths[1], kind[1]),
 
148
                file_id=file_id,
 
149
                text_modified=modified == 'modified'))
 
150
        else:
 
151
            self.modified.append(util.Container(
 
152
                filename=rich_filename(paths[1], kind),
 
153
                file_id=file_id))
 
154
 
 
155
# The lru_cache is not thread-safe, so we need a lock around it for
 
156
# all threads.
 
157
rev_info_memory_cache_lock = threading.RLock()
 
158
 
 
159
class RevInfoMemoryCache(object):
 
160
    """A store that validates values against the revids they were stored with.
 
161
 
 
162
    We use a unique key for each branch.
 
163
 
 
164
    The reason for not just using the revid as the key is so that when a new
 
165
    value is provided for a branch, we replace the old value used for the
 
166
    branch.
 
167
 
 
168
    There is another implementation of the same interface in
 
169
    loggerhead.changecache.RevInfoDiskCache.
 
170
    """
 
171
 
 
172
    def __init__(self, cache):
 
173
        self._cache = cache
 
174
 
 
175
    def get(self, key, revid):
 
176
        """Return the data associated with `key`, subject to a revid check.
 
177
 
 
178
        If a value was stored under `key`, with the same revid, return it.
 
179
        Otherwise return None.
 
180
        """
 
181
        rev_info_memory_cache_lock.acquire()
 
182
        try:
 
183
            cached = self._cache.get(key)
 
184
        finally:
 
185
            rev_info_memory_cache_lock.release()
 
186
        if cached is None:
 
187
            return None
 
188
        stored_revid, data = cached
 
189
        if revid == stored_revid:
 
190
            return data
 
191
        else:
 
192
            return None
 
193
 
 
194
    def set(self, key, revid, data):
 
195
        """Store `data` under `key`, to be checked against `revid` on get().
 
196
        """
 
197
        rev_info_memory_cache_lock.acquire()
 
198
        try:
 
199
            self._cache[key] = (revid, data)
 
200
        finally:
 
201
            rev_info_memory_cache_lock.release()
 
202
 
 
203
# Used to store locks that prevent multiple threads from building a 
 
204
# revision graph for the same branch at the same time, because that can
 
205
# cause severe performance issues that are so bad that the system seems
 
206
# to hang.
 
207
revision_graph_locks = {}
 
208
revision_graph_check_lock = threading.Lock()
 
209
 
 
210
class History(object):
 
211
    """Decorate a branch to provide information for rendering.
 
212
 
 
213
    History objects are expected to be short lived -- when serving a request
 
214
    for a particular branch, open it, read-lock it, wrap a History object
 
215
    around it, serve the request, throw the History object away, unlock the
 
216
    branch and throw it away.
 
217
 
 
218
    :ivar _file_change_cache: An object that caches information about the
 
219
        files that changed between two revisions.
 
220
    :ivar _rev_info: A list of information about revisions.  This is by far
 
221
        the most cryptic data structure in loggerhead.  At the top level, it
 
222
        is a list of 3-tuples [(merge-info, where-merged, parents)].
 
223
        `merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
 
224
        like a merged sorted list, but the revno is stringified.
 
225
        `where-merged` is a tuple of revisions that have this revision as a
 
226
        non-lefthand parent.  Finally, `parents` is just the usual list of
 
227
        parents of this revision.
 
228
    :ivar _rev_indices: A dictionary mapping each revision id to the index of
 
229
        the information about it in _rev_info.
 
230
    :ivar _revno_revid: A dictionary mapping stringified revnos to revision
 
231
        ids.
 
232
    """
 
233
 
 
234
    def _load_whole_history_data(self, caches, cache_key):
 
235
        """Set the attributes relating to the whole history of the branch.
 
236
 
 
237
        :param caches: a list of caches with interfaces like
 
238
            `RevInfoMemoryCache` and be ordered from fastest to slowest.
 
239
        :param cache_key: the key to use with the caches.
 
240
        """
 
241
        self._rev_indices = None
 
242
        self._rev_info = None
 
243
 
 
244
        missed_caches = []
 
245
        def update_missed_caches():
 
246
            for cache in missed_caches:
 
247
                cache.set(cache_key, self.last_revid, self._rev_info)
 
248
 
 
249
        # Theoretically, it's possible for two threads to race in creating
 
250
        # the Lock() object for their branch, so we put a lock around
 
251
        # creating the per-branch Lock().
 
252
        revision_graph_check_lock.acquire()
 
253
        try:
 
254
            if cache_key not in revision_graph_locks:
 
255
                revision_graph_locks[cache_key] = threading.Lock()
 
256
        finally:
 
257
            revision_graph_check_lock.release()
 
258
 
 
259
        revision_graph_locks[cache_key].acquire()
 
260
        try:
 
261
            for cache in caches:
 
262
                data = cache.get(cache_key, self.last_revid)
 
263
                if data is not None:
 
264
                    self._rev_info = data
 
265
                    update_missed_caches()
 
266
                    break
 
267
                else:
 
268
                    missed_caches.append(cache)
 
269
            else:
 
270
                whole_history_data = compute_whole_history_data(self._branch)
 
271
                self._rev_info, self._rev_indices = whole_history_data
 
272
                update_missed_caches()
 
273
        finally:
 
274
            revision_graph_locks[cache_key].release()
 
275
 
 
276
        if self._rev_indices is not None:
 
277
            self._revno_revid = {}
 
278
            for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
 
279
                self._revno_revid[revno_str] = revid
 
280
        else:
 
281
            self._revno_revid = {}
 
282
            self._rev_indices = {}
 
283
            for ((seq, revid, _, revno_str, _), _, _) in self._rev_info:
 
284
                self._rev_indices[revid] = seq
 
285
                self._revno_revid[revno_str] = revid
 
286
 
 
287
    def __init__(self, branch, whole_history_data_cache, file_cache=None,
 
288
                 revinfo_disk_cache=None, cache_key=None):
 
289
        assert branch.is_locked(), (
 
290
            "Can only construct a History object with a read-locked branch.")
 
291
        if file_cache is not None:
 
292
            self._file_change_cache = file_cache
 
293
            file_cache.history = self
 
294
        else:
 
295
            self._file_change_cache = None
61
296
        self._branch = branch
62
 
        self._history = branch.revision_history()
63
 
        self._revision_graph = branch.repository.get_revision_graph()
64
 
        self._last_revid = self._history[-1]
65
 
        
66
 
        self._full_history = []
67
 
        self._revision_info = {}
68
 
        self._revno_revid = {}
69
 
        self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
70
 
        count = 0
71
 
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
72
 
            self._full_history.append(revid)
73
 
            revno_str = '.'.join(str(n) for n in revno)
74
 
            self._revno_revid[revno_str] = revid
75
 
            self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
76
 
            count += 1
77
 
        self._count = count
78
 
 
79
 
        # cache merge info
80
 
        self._where_merged = {}
81
 
        for revid in self._revision_graph.keys():
82
 
            if not revid in self._full_history: 
83
 
                continue
84
 
            for parent in self._revision_graph[revid]:
85
 
                self._where_merged.setdefault(parent, set()).add(revid)
86
 
 
87
 
        log.info('built revision graph cache: %r secs' % (time.time() - z,))
88
 
        return self
89
 
    
90
 
    @classmethod
91
 
    def from_folder(cls, path):
92
 
        b = bzrlib.branch.Branch.open(path)
93
 
        return cls.from_branch(b)
94
 
 
95
 
    def out_of_date(self):
96
 
        if self._branch.revision_history()[-1] != self._last_revid:
97
 
            return True
98
 
        return False
99
 
 
100
 
    def use_cache(self, path):
101
 
        if not os.path.exists(path):
102
 
            os.mkdir(path)
103
 
        # keep a separate cache for the diffs, because they're very time-consuming to fetch.
104
 
        cachefile = os.path.join(path, 'changes')
105
 
        cachefile_diffs = os.path.join(path, 'changes-diffs')
106
 
        
107
 
        # why can't shelve allow 'cw'?
108
 
        if not os.path.exists(cachefile):
109
 
            self._change_cache = shelve.open(cachefile, 'c', protocol=2)
110
 
        else:
111
 
            self._change_cache = shelve.open(cachefile, 'w', protocol=2)
112
 
        if not os.path.exists(cachefile_diffs):
113
 
            self._change_cache_diffs = shelve.open(cachefile_diffs, 'c', protocol=2)
114
 
        else:
115
 
            self._change_cache_diffs = shelve.open(cachefile_diffs, 'w', protocol=2)
116
 
            
117
 
        # once we process a change (revision), it should be the same forever.
118
 
        log.info('Using change cache %s; %d, %d entries.' % (path, len(self._change_cache), len(self._change_cache_diffs)))
119
 
        self._cache_lock = threading.Lock()
120
 
        self._change_cache_filename = cachefile
121
 
        self._change_cache_diffs_filename = cachefile_diffs
122
 
    
123
 
    def dont_use_cache(self):
124
 
        # called when a new history object needs to be created.  we can't use
125
 
        # the cache files anymore; they belong to the new history object.
126
 
        self._cache_lock.acquire()
127
 
        try:
128
 
            if self._change_cache is None:
129
 
                return
130
 
            self._change_cache.close()
131
 
            self._change_cache_diffs.close()
132
 
            self._change_cache = None
133
 
            self._change_cache_diffs = None
134
 
        finally:
135
 
            self._cache_lock.release()
136
 
    
137
 
    def flush_cache(self):
138
 
        # shelve seems to need the file to be closed to save anything :(
139
 
        self._cache_lock.acquire()
140
 
        try:
141
 
            log.info('flush cache: %r (from %r)' % (len(self._change_cache), threading.currentThread()))
142
 
            if self._change_cache is None:
143
 
                return
144
 
            self._change_cache.sync()
145
 
            self._change_cache_diffs.sync()
146
 
        finally:
147
 
            self._cache_lock.release()
148
 
    
149
 
    last_revid = property(lambda self: self._last_revid, None, None)
150
 
    
151
 
    count = property(lambda self: self._count, None, None)
152
 
    
153
 
    def get_revision(self, revid):
154
 
        return self._branch.repository.get_revision(revid)
155
 
    
 
297
        self._branch_tags = None
 
298
        self._inventory_cache = {}
 
299
        self._branch_nick = self._branch.get_config().get_nickname()
 
300
        self.log = logging.getLogger('loggerhead.%s' % (self._branch_nick,))
 
301
 
 
302
        self.last_revid = branch.last_revision()
 
303
 
 
304
        caches = [RevInfoMemoryCache(whole_history_data_cache)]
 
305
        if revinfo_disk_cache:
 
306
            caches.append(revinfo_disk_cache)
 
307
        self._load_whole_history_data(caches, cache_key)
 
308
 
 
309
    @property
 
310
    def has_revisions(self):
 
311
        return not bzrlib.revision.is_null(self.last_revid)
 
312
 
 
313
    def get_config(self):
 
314
        return self._branch.get_config()
 
315
 
156
316
    def get_revno(self, revid):
157
 
        if revid not in self._revision_info:
 
317
        if revid not in self._rev_indices:
158
318
            # ghost parent?
159
319
            return 'unknown'
160
 
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
161
 
        return revno_str
162
 
 
163
 
    def get_sequence(self, revid):
164
 
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
165
 
        return seq
166
 
    
167
 
    def get_revision_history(self):
168
 
        return self._full_history
169
 
    
170
 
    def get_revid_sequence(self, revid_list, revid):
171
 
        """
172
 
        given a list of revision ids, return the sequence # of this revid in
173
 
        the list.
174
 
        """
175
 
        seq = 0
176
 
        for r in revid_list:
177
 
            if revid == r:
178
 
                return seq
179
 
            seq += 1
180
 
    
181
 
    def get_revids_from(self, revid_list, revid):
182
 
        """
183
 
        given a list of revision ids, yield revisions in graph order,
184
 
        starting from revid.  the list can be None if you just want to travel
185
 
        across all revisions.
186
 
        """
 
320
        seq = self._rev_indices[revid]
 
321
        revno = self._rev_info[seq][0][3]
 
322
        return revno
 
323
 
 
324
    def get_revids_from(self, revid_list, start_revid):
 
325
        """
 
326
        Yield the mainline (wrt start_revid) revisions that merged each
 
327
        revid in revid_list.
 
328
        """
 
329
        if revid_list is None:
 
330
            revid_list = [r[0][1] for r in self._rev_info]
 
331
        revid_set = set(revid_list)
 
332
        revid = start_revid
 
333
 
 
334
        def introduced_revisions(revid):
 
335
            r = set([revid])
 
336
            seq = self._rev_indices[revid]
 
337
            md = self._rev_info[seq][0][2]
 
338
            i = seq + 1
 
339
            while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
 
340
                r.add(self._rev_info[i][0][1])
 
341
                i += 1
 
342
            return r
187
343
        while True:
188
 
            if (revid_list is None) or (revid in revid_list):
 
344
            if bzrlib.revision.is_null(revid):
 
345
                return
 
346
            if introduced_revisions(revid) & revid_set:
189
347
                yield revid
190
 
            if not self._revision_graph.has_key(revid):
191
 
                return
192
 
            parents = self._revision_graph[revid]
 
348
            parents = self._rev_info[self._rev_indices[revid]][2]
193
349
            if len(parents) == 0:
194
350
                return
195
351
            revid = parents[0]
196
 
        
 
352
 
197
353
    def get_short_revision_history_by_fileid(self, file_id):
198
 
        # wow.  is this really the only way we can get this list?  by
199
 
        # man-handling the weave store directly? :-0
200
354
        # FIXME: would be awesome if we could get, for a folder, the list of
201
 
        # revisions where items within that folder changed.
202
 
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
203
 
        w_revids = w.versions()
204
 
        revids = [r for r in self._full_history if r in w_revids]
 
355
        # revisions where items within that folder changed.i
 
356
        possible_keys = [(file_id, revid) for revid in self._rev_indices]
 
357
        get_parent_map = self._branch.repository.texts.get_parent_map
 
358
        # We chunk the requests as this works better with GraphIndex.
 
359
        # See _filter_revisions_touching_file_id in bzrlib/log.py
 
360
        # for more information.
 
361
        revids = []
 
362
        chunk_size = 1000
 
363
        for start in xrange(0, len(possible_keys), chunk_size):
 
364
            next_keys = possible_keys[start:start + chunk_size]
 
365
            revids += [k[1] for k in get_parent_map(next_keys)]
 
366
        del possible_keys, next_keys
205
367
        return revids
206
368
 
207
 
    def get_navigation(self, revid, path):
208
 
        """
209
 
        Given an optional revid and optional path, return a (revlist, revid)
210
 
        for navigation through the current scope: from the revid (or the
211
 
        latest revision) back to the original revision.
212
 
        
213
 
        If path is None, the entire revision history is the list scope.
214
 
        If revid is None, the latest revision is used.
215
 
        """
216
 
        if revid is None:
217
 
            revid = self._last_revid
218
 
        if path is not None:
219
 
            # since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
220
 
            inv = self._branch.repository.get_revision_inventory(revid)
221
 
            revlist = list(self.get_short_revision_history_by_fileid(inv.path2id(path)))
 
369
    def get_revision_history_since(self, revid_list, date):
 
370
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
 
371
        # so start at midnight on 02-sep.
 
372
        date = date + datetime.timedelta(days=1)
 
373
        # our revid list is sorted in REVERSE date order,
 
374
        # so go thru some hoops here...
 
375
        revid_list.reverse()
 
376
        index = bisect.bisect(_RevListToTimestamps(revid_list,
 
377
                                                   self._branch.repository),
 
378
                              date)
 
379
        if index == 0:
 
380
            return []
 
381
        revid_list.reverse()
 
382
        index = -index
 
383
        return revid_list[index:]
 
384
 
 
385
    def get_search_revid_list(self, query, revid_list):
 
386
        """
 
387
        given a "quick-search" query, try a few obvious possible meanings:
 
388
 
 
389
            - revision id or # ("128.1.3")
 
390
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
 
391
iso style "yyyy-mm-dd")
 
392
            - comment text as a fallback
 
393
 
 
394
        and return a revid list that matches.
 
395
        """
 
396
        # FIXME: there is some silliness in this action.  we have to look up
 
397
        # all the relevant changes (time-consuming) only to return a list of
 
398
        # revids which will be used to fetch a set of changes again.
 
399
 
 
400
        # if they entered a revid, just jump straight there;
 
401
        # ignore the passed-in revid_list
 
402
        revid = self.fix_revid(query)
 
403
        if revid is not None:
 
404
            if isinstance(revid, unicode):
 
405
                revid = revid.encode('utf-8')
 
406
            changes = self.get_changes([revid])
 
407
            if (changes is not None) and (len(changes) > 0):
 
408
                return [revid]
 
409
 
 
410
        date = None
 
411
        m = self.us_date_re.match(query)
 
412
        if m is not None:
 
413
            date = datetime.datetime(util.fix_year(int(m.group(3))),
 
414
                                     int(m.group(1)),
 
415
                                     int(m.group(2)))
 
416
        else:
 
417
            m = self.earth_date_re.match(query)
 
418
            if m is not None:
 
419
                date = datetime.datetime(util.fix_year(int(m.group(3))),
 
420
                                         int(m.group(2)),
 
421
                                         int(m.group(1)))
 
422
            else:
 
423
                m = self.iso_date_re.match(query)
 
424
                if m is not None:
 
425
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
 
426
                                             int(m.group(2)),
 
427
                                             int(m.group(3)))
 
428
        if date is not None:
 
429
            if revid_list is None:
 
430
                # if no limit to the query was given,
 
431
                # search only the direct-parent path.
 
432
                revid_list = list(self.get_revids_from(None, self.last_revid))
 
433
            return self.get_revision_history_since(revid_list, date)
 
434
 
 
435
    revno_re = re.compile(r'^[\d\.]+$')
 
436
    # the date regex are without a final '$' so that queries like
 
437
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
 
438
    # them 90% of what they want instead of nothing at all.)
 
439
    us_date_re = re.compile(r'^(\d{1,2})/(\d{1,2})/(\d\d(\d\d?))')
 
440
    earth_date_re = re.compile(r'^(\d{1,2})-(\d{1,2})-(\d\d(\d\d?))')
 
441
    iso_date_re = re.compile(r'^(\d\d\d\d)-(\d\d)-(\d\d)')
 
442
 
 
443
    def fix_revid(self, revid):
 
444
        # if a "revid" is actually a dotted revno, convert it to a revid
 
445
        if revid is None:
 
446
            return revid
 
447
        if revid == 'head:':
 
448
            return self.last_revid
 
449
        try:
 
450
            if self.revno_re.match(revid):
 
451
                revid = self._revno_revid[revid]
 
452
        except KeyError:
 
453
            raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
 
454
        return revid
 
455
 
 
456
    def get_file_view(self, revid, file_id):
 
457
        """
 
458
        Given a revid and optional path, return a (revlist, revid) for
 
459
        navigation through the current scope: from the revid (or the latest
 
460
        revision) back to the original revision.
 
461
 
 
462
        If file_id is None, the entire revision history is the list scope.
 
463
        """
 
464
        if revid is None:
 
465
            revid = self.last_revid
 
466
        if file_id is not None:
 
467
            # since revid is 'start_revid', possibly should start the path
 
468
            # tracing from revid... FIXME
 
469
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
 
470
            revlist = list(self.get_revids_from(revlist, revid))
222
471
        else:
223
472
            revlist = list(self.get_revids_from(None, revid))
224
 
        if revid is None:
225
 
            revid = revlist[0]
226
 
        return revlist, revid
 
473
        return revlist
 
474
 
 
475
    def get_view(self, revid, start_revid, file_id, query=None):
 
476
        """
 
477
        use the URL parameters (revid, start_revid, file_id, and query) to
 
478
        determine the revision list we're viewing (start_revid, file_id, query)
 
479
        and where we are in it (revid).
 
480
 
 
481
            - if a query is given, we're viewing query results.
 
482
            - if a file_id is given, we're viewing revisions for a specific
 
483
              file.
 
484
            - if a start_revid is given, we're viewing the branch from a
 
485
              specific revision up the tree.
 
486
 
 
487
        these may be combined to view revisions for a specific file, from
 
488
        a specific revision, with a specific search query.
 
489
 
 
490
        returns a new (revid, start_revid, revid_list) where:
 
491
 
 
492
            - revid: current position within the view
 
493
            - start_revid: starting revision of this view
 
494
            - revid_list: list of revision ids for this view
 
495
 
 
496
        file_id and query are never changed so aren't returned, but they may
 
497
        contain vital context for future url navigation.
 
498
        """
 
499
        if start_revid is None:
 
500
            start_revid = self.last_revid
 
501
 
 
502
        if query is None:
 
503
            revid_list = self.get_file_view(start_revid, file_id)
 
504
            if revid is None:
 
505
                revid = start_revid
 
506
            if revid not in revid_list:
 
507
                # if the given revid is not in the revlist, use a revlist that
 
508
                # starts at the given revid.
 
509
                revid_list = self.get_file_view(revid, file_id)
 
510
                start_revid = revid
 
511
            return revid, start_revid, revid_list
 
512
 
 
513
        # potentially limit the search
 
514
        if file_id is not None:
 
515
            revid_list = self.get_file_view(start_revid, file_id)
 
516
        else:
 
517
            revid_list = None
 
518
        revid_list = search.search_revisions(self._branch, query)
 
519
        if revid_list and len(revid_list) > 0:
 
520
            if revid not in revid_list:
 
521
                revid = revid_list[0]
 
522
            return revid, start_revid, revid_list
 
523
        else:
 
524
            # XXX: This should return a message saying that the search could
 
525
            # not be completed due to either missing the plugin or missing a
 
526
            # search index.
 
527
            return None, None, []
227
528
 
228
529
    def get_inventory(self, revid):
229
 
        return self._branch.repository.get_revision_inventory(revid)
230
 
 
231
 
    def get_where_merged(self, revid):
232
 
        try:
233
 
            return self._where_merged[revid]
234
 
        except:
235
 
            return []
236
 
    
 
530
        if revid not in self._inventory_cache:
 
531
            self._inventory_cache[revid] = (
 
532
                self._branch.repository.get_inventory(revid))
 
533
        return self._inventory_cache[revid]
 
534
 
 
535
    def get_path(self, revid, file_id):
 
536
        if (file_id is None) or (file_id == ''):
 
537
            return ''
 
538
        path = self.get_inventory(revid).id2path(file_id)
 
539
        if (len(path) > 0) and not path.startswith('/'):
 
540
            path = '/' + path
 
541
        return path
 
542
 
 
543
    def get_file_id(self, revid, path):
 
544
        if (len(path) > 0) and not path.startswith('/'):
 
545
            path = '/' + path
 
546
        return self.get_inventory(revid).path2id(path)
 
547
 
237
548
    def get_merge_point_list(self, revid):
238
549
        """
239
550
        Return the list of revids that have merged this node.
240
551
        """
241
 
        if revid in self._history:
 
552
        if '.' not in self.get_revno(revid):
242
553
            return []
243
 
        
 
554
 
244
555
        merge_point = []
245
556
        while True:
246
 
            children = self.get_where_merged(revid)
 
557
            children = self._rev_info[self._rev_indices[revid]][1]
247
558
            nexts = []
248
559
            for child in children:
249
 
                child_parents = self._revision_graph[child]
 
560
                child_parents = self._rev_info[self._rev_indices[child]][2]
250
561
                if child_parents[0] == revid:
251
562
                    nexts.append(child)
252
563
                else:
263
574
                merge_point.extend(merge_point_next)
264
575
 
265
576
            revid = nexts[0]
266
 
            
 
577
 
267
578
    def simplify_merge_point_list(self, revids):
268
579
        """if a revision is already merged, don't show further merge points"""
269
580
        d = {}
272
583
            revnol = revno.split(".")
273
584
            revnos = ".".join(revnol[:-2])
274
585
            revnolast = int(revnol[-1])
275
 
            if d.has_key(revnos):
 
586
            if revnos in d:
276
587
                m = d[revnos][0]
277
588
                if revnolast < m:
278
 
                    d[revnos] = ( revnolast, revid )
279
 
            else:
280
 
                d[revnos] = ( revnolast, revid )
281
 
 
282
 
        return [ d[revnos][1] for revnos in d.keys() ]
283
 
            
284
 
    def get_changelist(self, revid_list):
285
 
        for revid in revid_list:
286
 
            yield self.get_change(revid)
287
 
    
288
 
    def get_change(self, revid, get_diffs=False):
289
 
        if self._change_cache is None:
290
 
            return self._get_change(revid, get_diffs)
291
 
 
292
 
        # if the revid is in unicode, use the utf-8 encoding as the key
293
 
        srevid = revid
294
 
        if isinstance(revid, unicode):
295
 
            srevid = revid.encode('utf-8')
296
 
        self._cache_lock.acquire()
297
 
        try:
298
 
            if get_diffs:
299
 
                cache = self._change_cache_diffs
300
 
            else:
301
 
                cache = self._change_cache
302
 
            
303
 
            if srevid in cache:
304
 
                c = cache[srevid]
305
 
            else:
306
 
                if get_diffs and (srevid in self._change_cache):
307
 
                    # salvage the non-diff entry for a jump-start
308
 
                    c = self._change_cache[srevid]
309
 
                    if len(change.parents) == 0:
310
 
                        left_parent = None
311
 
                    else:
312
 
                        left_parent = change.parents[0].revid
313
 
                    c.changes = self.diff_revisions(revid, left_parent, get_diffs=True)
314
 
                    cache[srevid] = c
315
 
                else:
316
 
                    #log.debug('Entry cache miss: %r' % (revid,))
317
 
                    c = self._get_change(revid, get_diffs=get_diffs)
318
 
                    cache[srevid] = c
319
 
            
320
 
            # some data needs to be recalculated each time, because it may
321
 
            # change as new revisions are added.
322
 
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
323
 
            c.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
324
 
            
325
 
            return c
326
 
        finally:
327
 
            self._cache_lock.release()
328
 
    
329
 
    def _get_change(self, revid, get_diffs=False):
330
 
        try:
331
 
            rev = self._branch.repository.get_revision(revid)
332
 
        except (KeyError, bzrlib.errors.NoSuchRevision):
333
 
            # ghosted parent?
334
 
            entry = {
335
 
                'revid': 'missing',
336
 
                'revno': '',
337
 
                'date': datetime.datetime.fromtimestamp(0),
338
 
                'author': 'missing',
339
 
                'branch_nick': None,
340
 
                'short_comment': 'missing',
341
 
                'comment': 'missing',
342
 
                'comment_clean': 'missing',
343
 
                'parents': [],
344
 
                'merge_points': [],
345
 
                'changes': [],
346
 
            }
347
 
            log.error('ghost entry: %r' % (revid,))
348
 
            return util.Container(entry)
349
 
            
350
 
        commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
351
 
        
352
 
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
353
 
 
354
 
        if len(parents) == 0:
355
 
            left_parent = None
356
 
        else:
357
 
            left_parent = rev.parent_ids[0]
358
 
        
359
 
        message = rev.message.splitlines()
360
 
        if len(message) == 1:
361
 
            # robey-style 1-line long message
362
 
            message = textwrap.wrap(message[0])
363
 
        
364
 
        # make short form of commit message
365
 
        short_message = message[0]
366
 
        if len(short_message) > 60:
367
 
            short_message = short_message[:60] + '...'
 
589
                    d[revnos] = (revnolast, revid)
 
590
            else:
 
591
                d[revnos] = (revnolast, revid)
 
592
 
 
593
        return [revid for (_, revid) in d.itervalues()]
 
594
 
 
595
    def add_branch_nicks(self, change):
 
596
        """
 
597
        given a 'change', fill in the branch nicks on all parents and merge
 
598
        points.
 
599
        """
 
600
        fetch_set = set()
 
601
        for p in change.parents:
 
602
            fetch_set.add(p.revid)
 
603
        for p in change.merge_points:
 
604
            fetch_set.add(p.revid)
 
605
        p_changes = self.get_changes(list(fetch_set))
 
606
        p_change_dict = dict([(c.revid, c) for c in p_changes])
 
607
        for p in change.parents:
 
608
            if p.revid in p_change_dict:
 
609
                p.branch_nick = p_change_dict[p.revid].branch_nick
 
610
            else:
 
611
                p.branch_nick = '(missing)'
 
612
        for p in change.merge_points:
 
613
            if p.revid in p_change_dict:
 
614
                p.branch_nick = p_change_dict[p.revid].branch_nick
 
615
            else:
 
616
                p.branch_nick = '(missing)'
 
617
 
 
618
    def get_changes(self, revid_list):
 
619
        """Return a list of changes objects for the given revids.
 
620
 
 
621
        Revisions not present and NULL_REVISION will be ignored.
 
622
        """
 
623
        changes = self.get_changes_uncached(revid_list)
 
624
        if len(changes) == 0:
 
625
            return changes
 
626
 
 
627
        # some data needs to be recalculated each time, because it may
 
628
        # change as new revisions are added.
 
629
        for change in changes:
 
630
            merge_revids = self.simplify_merge_point_list(
 
631
                               self.get_merge_point_list(change.revid))
 
632
            change.merge_points = [
 
633
                util.Container(revid=r,
 
634
                revno=self.get_revno(r)) for r in merge_revids]
 
635
            if len(change.parents) > 0:
 
636
                change.parents = [util.Container(revid=r,
 
637
                    revno=self.get_revno(r)) for r in change.parents]
 
638
            change.revno = self.get_revno(change.revid)
 
639
 
 
640
        parity = 0
 
641
        for change in changes:
 
642
            change.parity = parity
 
643
            parity ^= 1
 
644
 
 
645
        return changes
 
646
 
 
647
    def get_changes_uncached(self, revid_list):
 
648
        # FIXME: deprecated method in getting a null revision
 
649
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
 
650
                            revid_list)
 
651
        parent_map = self._branch.repository.get_graph().get_parent_map(
 
652
                         revid_list)
 
653
        # We need to return the answer in the same order as the input,
 
654
        # less any ghosts.
 
655
        present_revids = [revid for revid in revid_list
 
656
                          if revid in parent_map]
 
657
        rev_list = self._branch.repository.get_revisions(present_revids)
 
658
 
 
659
        return [self._change_from_revision(rev) for rev in rev_list]
 
660
 
 
661
    def _change_from_revision(self, revision):
 
662
        """
 
663
        Given a bzrlib Revision, return a processed "change" for use in
 
664
        templates.
 
665
        """
 
666
        message, short_message = clean_message(revision.message)
 
667
 
 
668
        if self._branch_tags is None:
 
669
            self._branch_tags = self._branch.tags.get_reverse_tag_dict()
 
670
 
 
671
        revtags = None
 
672
        if revision.revision_id in self._branch_tags:
 
673
          revtags = ', '.join(self._branch_tags[revision.revision_id])
368
674
 
369
675
        entry = {
370
 
            'revid': revid,
371
 
            'revno': self.get_revno(revid),
372
 
            'date': commit_time,
373
 
            'author': rev.committer,
374
 
            'branch_nick': rev.properties.get('branch-nick', None),
 
676
            'revid': revision.revision_id,
 
677
            'date': datetime.datetime.fromtimestamp(revision.timestamp),
 
678
            'utc_date': datetime.datetime.utcfromtimestamp(revision.timestamp),
 
679
            'authors': revision.get_apparent_authors(),
 
680
            'branch_nick': revision.properties.get('branch-nick', None),
375
681
            'short_comment': short_message,
376
 
            'comment': rev.message,
 
682
            'comment': revision.message,
377
683
            'comment_clean': [util.html_clean(s) for s in message],
378
 
            'parents': parents,
379
 
            'changes': self.diff_revisions(revid, left_parent, get_diffs=get_diffs),
 
684
            'parents': revision.parent_ids,
 
685
            'bugs': [bug.split()[0] for bug in revision.properties.get('bugs', '').splitlines()],
 
686
            'tags': revtags,
380
687
        }
 
688
        if isinstance(revision, bzrlib.foreign.ForeignRevision):
 
689
            foreign_revid, mapping = (rev.foreign_revid, rev.mapping)
 
690
        elif ":" in revision.revision_id:
 
691
            try:
 
692
                foreign_revid, mapping = \
 
693
                    bzrlib.foreign.foreign_vcs_registry.parse_revision_id(
 
694
                        revision.revision_id)
 
695
            except bzrlib.errors.InvalidRevisionId:
 
696
                foreign_revid = None
 
697
                mapping = None
 
698
        else:
 
699
            foreign_revid = None
 
700
        if foreign_revid is not None:
 
701
            entry["foreign_vcs"] = mapping.vcs.abbreviation
 
702
            entry["foreign_revid"] = mapping.vcs.show_foreign_revid(foreign_revid)
381
703
        return util.Container(entry)
382
 
    
383
 
    def scan_range(self, revlist, revid, pagesize=20):
384
 
        """
385
 
        yield a list of (label, title, revid) for a scan range through the full
386
 
        branch history, centered around the given revid.
387
 
        
388
 
        example: [ ('(425)', 'Latest', 'rrrr'), ('+1', 'Forward 1', 'rrrr'), ...
389
 
                   ('-300', 'Back 300', 'rrrr'), ('(1)', 'Oldest', 'first-revid') ]
390
 
        """
391
 
        count = len(revlist)
392
 
        pos = self.get_revid_sequence(revlist, revid)
393
 
        if pos < count - 1:
394
 
            yield ('<', 'Back %d' % (pagesize,),
395
 
                   revlist[min(count - 1, pos + pagesize)])
396
 
        else:
397
 
            yield ('<', None, None)
398
 
        yield ('(1)', 'Oldest', revlist[-1])
399
 
        for offset in reversed([-x for x in util.scan_range(pos, count)]):
400
 
            if offset < 0:
401
 
                title = 'Back %d' % (-offset,)
402
 
            else:
403
 
                title = 'Forward %d' % (offset,)
404
 
            yield ('%+d' % (offset,), title, revlist[pos - offset])
405
 
        yield ('(%d)' % (len(revlist),) , 'Latest', revlist[0])
406
 
        if pos > 0:
407
 
            yield ('>', 'Forward %d' % (pagesize,),
408
 
                   revlist[max(0, pos - pagesize)])
409
 
        else:
410
 
            yield ('>', None, None)
411
 
    
412
 
    def get_revlist_offset(self, revlist, revid, offset):
413
 
        count = len(revlist)
414
 
        pos = self.get_revid_sequence(revlist, revid)
415
 
        if offset < 0:
416
 
            return revlist[max(0, pos + offset)]
417
 
        return revlist[min(count - 1, pos + offset)]
418
 
    
419
 
    def diff_revisions(self, revid, otherrevid, get_diffs=True):
420
 
        """
421
 
        Return a nested data structure containing the changes between two
422
 
        revisions::
423
 
        
424
 
            added: list(filename),
425
 
            renamed: list((old_filename, new_filename)),
426
 
            deleted: list(filename),
 
704
 
 
705
    def get_file_changes_uncached(self, entry):
 
706
        if entry.parents:
 
707
            old_revid = entry.parents[0].revid
 
708
        else:
 
709
            old_revid = bzrlib.revision.NULL_REVISION
 
710
        return self.file_changes_for_revision_ids(old_revid, entry.revid)
 
711
 
 
712
    def get_file_changes(self, entry):
 
713
        if self._file_change_cache is None:
 
714
            return self.get_file_changes_uncached(entry)
 
715
        else:
 
716
            return self._file_change_cache.get_file_changes(entry)
 
717
 
 
718
    def add_changes(self, entry):
 
719
        changes = self.get_file_changes(entry)
 
720
        entry.changes = changes
 
721
 
 
722
    def get_file(self, file_id, revid):
 
723
        """Returns (path, filename, file contents)"""
 
724
        inv = self.get_inventory(revid)
 
725
        inv_entry = inv[file_id]
 
726
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
 
727
        path = inv.id2path(file_id)
 
728
        if not path.startswith('/'):
 
729
            path = '/' + path
 
730
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
 
731
 
 
732
    def file_changes_for_revision_ids(self, old_revid, new_revid):
 
733
        """
 
734
        Return a nested data structure containing the changes in a delta::
 
735
 
 
736
            added: list((filename, file_id)),
 
737
            renamed: list((old_filename, new_filename, file_id)),
 
738
            deleted: list((filename, file_id)),
427
739
            modified: list(
428
740
                filename: str,
429
 
                chunks: list(
430
 
                    diff: list(
431
 
                        old_lineno: int,
432
 
                        new_lineno: int,
433
 
                        type: str('context', 'delete', or 'insert'),
434
 
                        line: str,
435
 
                    ),
436
 
                ),
437
 
            )
438
 
        
439
 
        if C{get_diffs} is false, the C{chunks} will be omitted.
440
 
        """
441
 
 
442
 
        new_tree = self._branch.repository.revision_tree(revid)
443
 
        old_tree = self._branch.repository.revision_tree(otherrevid)
444
 
        delta = new_tree.changes_from(old_tree)
445
 
        
446
 
        added = []
447
 
        modified = []
448
 
        renamed = []
449
 
        removed = []
450
 
        
451
 
        def rich_filename(path, kind):
452
 
            if kind == 'directory':
453
 
                path += '/'
454
 
            if kind == 'symlink':
455
 
                path += '@'
456
 
            return path
457
 
        
458
 
        def tree_lines(tree, fid):
459
 
            if not fid in tree:
460
 
                return []
461
 
            tree_file = bzrlib.textfile.text_file(tree.get_file(fid))
462
 
            return tree_file.readlines()
463
 
        
464
 
        def process_diff(diff):
465
 
            chunks = []
466
 
            chunk = None
467
 
            for line in diff.splitlines():
468
 
                if len(line) == 0:
469
 
                    continue
470
 
                if line.startswith('+++ ') or line.startswith('--- '):
471
 
                    continue
472
 
                if line.startswith('@@ '):
473
 
                    # new chunk
474
 
                    if chunk is not None:
475
 
                        chunks.append(chunk)
476
 
                    chunk = util.Container()
477
 
                    chunk.diff = []
478
 
                    lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
479
 
                    old_lineno = lines[0]
480
 
                    new_lineno = lines[1]
481
 
                elif line.startswith(' '):
482
 
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
483
 
                                                     type='context', line=util.html_clean(line[1:])))
484
 
                    old_lineno += 1
485
 
                    new_lineno += 1
486
 
                elif line.startswith('+'):
487
 
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
488
 
                                                     type='insert', line=util.html_clean(line[1:])))
489
 
                    new_lineno += 1
490
 
                elif line.startswith('-'):
491
 
                    chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
492
 
                                                     type='delete', line=util.html_clean(line[1:])))
493
 
                    old_lineno += 1
494
 
                else:
495
 
                    chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
496
 
                                                     type='unknown', line=util.html_clean(repr(line))))
497
 
            if chunk is not None:
498
 
                chunks.append(chunk)
499
 
            return chunks
500
 
                    
501
 
        def handle_modify(old_path, new_path, fid, kind):
502
 
            if not get_diffs:
503
 
                modified.append(util.Container(filename=rich_filename(new_path, kind)))
504
 
                return
505
 
            old_lines = tree_lines(old_tree, fid)
506
 
            new_lines = tree_lines(new_tree, fid)
507
 
            buffer = StringIO()
508
 
            bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
509
 
            diff = buffer.getvalue()
510
 
            modified.append(util.Container(filename=rich_filename(new_path, kind), chunks=process_diff(diff)))
511
 
 
512
 
        for path, fid, kind in delta.added:
513
 
            added.append(rich_filename(path, kind))
514
 
        
515
 
        for path, fid, kind, text_modified, meta_modified in delta.modified:
516
 
            handle_modify(path, path, fid, kind)
517
 
        
518
 
        for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
519
 
            renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind)))
520
 
            if meta_modified or text_modified:
521
 
                handle_modify(oldpath, newpath, fid, kind)
522
 
        
523
 
        for path, fid, kind in delta.removed:
524
 
            removed.append(rich_filename(path, kind))
525
 
        
526
 
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
527
 
 
528
 
    def get_filelist(self, inv, path):
529
 
        """
530
 
        return the list of all files (and their attributes) within a given
531
 
        path subtree.
532
 
        """
533
 
        while path.endswith('/'):
534
 
            path = path[:-1]
535
 
        if path.startswith('/'):
536
 
            path = path[1:]
537
 
        parity = 0
538
 
        for filepath, entry in inv.entries():
539
 
            if posixpath.dirname(filepath) != path:
540
 
                continue
541
 
            filename = posixpath.basename(filepath)
542
 
            rich_filename = filename
543
 
            pathname = filename
544
 
            if entry.kind == 'directory':
545
 
                pathname += '/'
546
 
            
547
 
            # last change:
548
 
            revid = entry.revision
549
 
            
550
 
            yield util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
551
 
                                 pathname=pathname, revid=revid, revno=self.get_revno(revid), parity=parity)
552
 
            parity ^= 1
553
 
        pass
554
 
 
555
 
    def annotate_file(self, file_id, revid):
556
 
        z = time.time()
557
 
        lineno = 1
558
 
        parity = 0
559
 
        
560
 
        file_revid = self.get_inventory(revid)[file_id].revision
561
 
        oldvalues = None
562
 
        revision_cache = {}
563
 
        
564
 
        # because we cache revision metadata ourselves, it's actually much
565
 
        # faster to call 'annotate_iter' on the weave directly than it is to
566
 
        # ask bzrlib to annotate for us.
567
 
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
568
 
        last_line_revid = None
569
 
        for line_revid, text in w.annotate_iter(file_revid):
570
 
            if line_revid == last_line_revid:
571
 
                # remember which lines have a new revno and which don't
572
 
                status = 'same'
573
 
            else:
574
 
                status = 'changed'
575
 
                parity ^= 1
576
 
                last_line_revid = line_revid
577
 
                change = revision_cache.get(line_revid, None)
578
 
                if change is None:
579
 
                    change = self.get_change(line_revid)
580
 
                    revision_cache[line_revid] = change
581
 
                trunc_revno = change.revno
582
 
                if len(trunc_revno) > 10:
583
 
                    trunc_revno = trunc_revno[:9] + '...'
584
 
                
585
 
            yield util.Container(parity=parity, lineno=lineno, status=status,
586
 
                                 trunc_revno=trunc_revno, change=change, text=util.html_clean(text))
587
 
            lineno += 1
588
 
        
589
 
        log.debug('annotate: %r secs' % (time.time() - z,))
 
741
                file_id: str,
 
742
            ),
 
743
            text_changes: list((filename, file_id)),
 
744
        """
 
745
        repo = self._branch.repository
 
746
        if (bzrlib.revision.is_null(old_revid) or
 
747
            bzrlib.revision.is_null(new_revid)):
 
748
            old_tree, new_tree = map(
 
749
                repo.revision_tree, [old_revid, new_revid])
 
750
        else:
 
751
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
 
752
 
 
753
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
 
754
 
 
755
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
 
756
 
 
757
        return util.Container(
 
758
            added=sorted(reporter.added, key=lambda x: x.filename),
 
759
            renamed=sorted(reporter.renamed, key=lambda x: x.new_filename),
 
760
            removed=sorted(reporter.removed, key=lambda x: x.filename),
 
761
            modified=sorted(reporter.modified, key=lambda x: x.filename),
 
762
            text_changes=sorted(reporter.text_changes, key=lambda x: x.filename))