~loggerhead-team/loggerhead/trunk

1 by Robey Pointer
initial checkin
1
#
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
2
# Copyright (C) 2008  Canonical Ltd.
185 by Martin Albisetti
* Clarify License and add copyright to all file headers (John Arbash Meinel)
3
#                     (Authored by Martin Albisetti <argentina@gmail.com>)
1 by Robey Pointer
initial checkin
4
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
23 by Robey Pointer
lots of little changes:
5
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
48 by Robey Pointer
the big migration of branch-specific data to a BranchView object: actually
6
# Copyright (C) 2005  Jake Edge <jake@edge2.net>
7
# Copyright (C) 2005  Matt Mackall <mpm@selenic.com>
1 by Robey Pointer
initial checkin
8
#
9
# This program is free software; you can redistribute it and/or modify
10
# it under the terms of the GNU General Public License as published by
11
# the Free Software Foundation; either version 2 of the License, or
12
# (at your option) any later version.
13
#
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
# GNU General Public License for more details.
18
#
19
# You should have received a copy of the GNU General Public License
20
# along with this program; if not, write to the Free Software
21
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22
#
23
48 by Robey Pointer
the big migration of branch-specific data to a BranchView object: actually
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
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
31
import bisect
1 by Robey Pointer
initial checkin
32
import datetime
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
33
import logging
332.2.3 by Michael Hudson
fairly tortuous two level caching
34
import marshal
23 by Robey Pointer
lots of little changes:
35
import re
1 by Robey Pointer
initial checkin
36
import textwrap
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
37
import threading
38
import time
219.2.6 by Martin Albisetti
Start switching from fileids to paths in navigation
39
import urllib
2 by Robey Pointer
add revision page, put some of the config in the config file
40
from StringIO import StringIO
1 by Robey Pointer
initial checkin
41
128.10.1 by Martin Albisetti
* Start integration with bzr-search
42
from loggerhead import search
24 by Robey Pointer
figured out how to make my own separate config file like BzrInspect, and
43
from loggerhead import util
179.1.10 by Michael Hudson
rename some things, change History to be more conventionally constructed
44
from loggerhead.wholehistory import compute_whole_history_data
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
45
1 by Robey Pointer
initial checkin
46
import bzrlib
47
import bzrlib.branch
305.1.8 by Michael Hudson
finally, use iter_changes and report_changes, not changes_from.
48
import bzrlib.delta
2 by Robey Pointer
add revision page, put some of the config in the config file
49
import bzrlib.diff
1 by Robey Pointer
initial checkin
50
import bzrlib.errors
332.2.5 by Michael Hudson
refactor in the direction of maybe making sense one day
51
import bzrlib.lru_cache
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
52
import bzrlib.progress
128.1.60 by Michael Hudson
respond to review comments
53
import bzrlib.revision
259.1.1 by Matt Nordhoff
Fix missing import for bzrlib.textfile
54
import bzrlib.textfile
1 by Robey Pointer
initial checkin
55
import bzrlib.tsort
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
56
import bzrlib.ui
1 by Robey Pointer
initial checkin
57
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
58
# bzrlib's UIFactory is not thread-safe
59
uihack = threading.local()
60
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
61
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
62
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
63
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
64
    def nested_progress_bar(self):
65
        if getattr(uihack, '_progress_bar_stack', None) is None:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
66
            pbs = bzrlib.progress.ProgressBarStack(
67
                      klass=bzrlib.progress.DummyProgress)
68
            uihack._progress_bar_stack = pbs
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
69
        return uihack._progress_bar_stack.get_nested()
70
71
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
72
74 by Robey Pointer
add the ability to auto-publish anything found under a particular folder.
73
def is_branch(folder):
74
    try:
75
        bzrlib.branch.Branch.open(folder)
76
        return True
77
    except:
78
        return False
79
80
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
81
def clean_message(message):
138.1.3 by Michael Hudson
clean up various comments and add docstrings
82
    """Clean up a commit message and return it and a short (1-line) version.
83
84
    Commit messages that are long single lines are reflowed using the textwrap
85
    module (Robey, the original author of this code, apparently favored this
86
    style of message).
87
    """
279.1.1 by Colin Watson
Leading blank lines in commit messages no longer result in an empty summary.
88
    message = message.lstrip().splitlines()
138.1.3 by Michael Hudson
clean up various comments and add docstrings
89
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
90
    if len(message) == 1:
91
        message = textwrap.wrap(message[0])
138.1.3 by Michael Hudson
clean up various comments and add docstrings
92
138.1.2 by Michael Hudson
test and fix for the problem
93
    if len(message) == 0:
138.1.3 by Michael Hudson
clean up various comments and add docstrings
94
        # We can end up where when (a) the commit message was empty or (b)
95
        # when the message consisted entirely of whitespace, in which case
96
        # textwrap.wrap() returns an empty list.
97
        return [''], ''
128.1.46 by Michael Hudson
delete-trailing-whitespace
98
138.1.3 by Michael Hudson
clean up various comments and add docstrings
99
    # Make short form of commit message.
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
100
    short_message = message[0]
128.6.12 by Michael Hudson
go back to 60 character 'short' commit messages
101
    if len(short_message) > 60:
102
        short_message = short_message[:60] + '...'
128.1.46 by Michael Hudson
delete-trailing-whitespace
103
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
104
    return message, short_message
105
106
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
107
def rich_filename(path, kind):
108
    if kind == 'directory':
109
        path += '/'
110
    if kind == 'symlink':
111
        path += '@'
112
    return path
128.1.46 by Michael Hudson
delete-trailing-whitespace
113
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
114
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
115
# from bzrlib
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
116
117
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
118
class _RevListToTimestamps(object):
119
    """This takes a list of revisions, and allows you to bisect by date"""
120
121
    __slots__ = ['revid_list', 'repository']
122
123
    def __init__(self, revid_list, repository):
124
        self.revid_list = revid_list
125
        self.repository = repository
126
127
    def __getitem__(self, index):
128
        """Get the date of the index'd item"""
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
129
        return datetime.datetime.fromtimestamp(self.repository.get_revision(
130
                   self.revid_list[index]).timestamp)
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
131
132
    def __len__(self):
133
        return len(self.revid_list)
134
305.1.8 by Michael Hudson
finally, use iter_changes and report_changes, not changes_from.
135
class FileChangeReporter(object):
308.1.3 by Michael Hudson
push all handling of revision trees into history
136
    def __init__(self, old_inv, new_inv):
305.1.8 by Michael Hudson
finally, use iter_changes and report_changes, not changes_from.
137
        self.added = []
138
        self.modified = []
139
        self.renamed = []
140
        self.removed = []
141
        self.text_changes = []
308.1.3 by Michael Hudson
push all handling of revision trees into history
142
        self.old_inv = old_inv
143
        self.new_inv = new_inv
144
145
    def revid(self, inv, file_id):
146
        try:
147
            return inv[file_id].revision
148
        except bzrlib.errors.NoSuchId:
149
            return 'null:'
150
305.1.8 by Michael Hudson
finally, use iter_changes and report_changes, not changes_from.
151
    def report(self, file_id, paths, versioned, renamed, modified,
152
               exe_change, kind):
153
        if modified not in ('unchanged', 'kind changed'):
312 by Michael Hudson
fix thinko on revision pages for removed files
154
            if versioned == 'removed':
155
                filename = rich_filename(paths[0], kind[0])
156
            else:
157
                filename = rich_filename(paths[1], kind[1])
305.1.8 by Michael Hudson
finally, use iter_changes and report_changes, not changes_from.
158
            self.text_changes.append(util.Container(
312 by Michael Hudson
fix thinko on revision pages for removed files
159
                filename=filename, file_id=file_id,
308.1.3 by Michael Hudson
push all handling of revision trees into history
160
                old_revision=self.revid(self.old_inv, file_id),
161
                new_revision=self.revid(self.new_inv, file_id)))
305.1.8 by Michael Hudson
finally, use iter_changes and report_changes, not changes_from.
162
        if versioned == 'added':
163
            self.added.append(util.Container(
164
                filename=rich_filename(paths[1], kind),
165
                file_id=file_id, kind=kind[1]))
166
        elif versioned == 'removed':
167
            self.removed.append(util.Container(
305.1.15 by Michael Hudson
grr
168
                filename=rich_filename(paths[0], kind),
305.1.8 by Michael Hudson
finally, use iter_changes and report_changes, not changes_from.
169
                file_id=file_id, kind=kind[0]))
170
        elif renamed:
171
            self.renamed.append(util.Container(
172
                old_filename=rich_filename(paths[0], kind[0]),
173
                new_filename=rich_filename(paths[1], kind[1]),
174
                file_id=file_id,
175
                text_modified=modified == 'modified'))
176
        else:
177
            self.modified.append(util.Container(
178
                filename=rich_filename(paths[1], kind),
179
                file_id=file_id))
180
128.1.26 by Michael Hudson
same trick for revision.kid. still way too slow for big diffs though
181
332.2.5 by Michael Hudson
refactor in the direction of maybe making sense one day
182
class RevInfoMemoryCache(object):
332.2.8 by Michael Hudson
docstrings (omg!!)
183
    """A store that validates values against the revids they were stored with.
184
185
    We use a unique key for each branch.
186
187
    The reason for not just using the revid as the key is so that when a new
188
    value is provided for a branch, we replace the old value used for the
189
    branch.
190
191
    There is another implementation of the same interface in
192
    loggerhead.changecache.RevInfoDiskCache.
193
    """
194
332.2.5 by Michael Hudson
refactor in the direction of maybe making sense one day
195
    def __init__(self, cache):
196
        self._cache = cache
197
198
    def get(self, key, revid):
332.2.8 by Michael Hudson
docstrings (omg!!)
199
        """Return the data associated with `key`, subject to a revid check.
200
201
        If a value was stored under `key`, with the same revid, return it.
202
        Otherwise return None.
203
        """
332.2.5 by Michael Hudson
refactor in the direction of maybe making sense one day
204
        cached = self._cache.get(key)
205
        if cached is None:
206
            return None
207
        stored_revid, data = cached
208
        if revid == stored_revid:
209
            return data
210
        else:
211
            return None
212
213
    def set(self, key, revid, data):
332.2.8 by Michael Hudson
docstrings (omg!!)
214
        """Store `data` under `key`, to be checked against `revid` on get().
215
        """
332.2.5 by Michael Hudson
refactor in the direction of maybe making sense one day
216
        self._cache[key] = (revid, data)
217
218
1 by Robey Pointer
initial checkin
219
class History (object):
179.1.10 by Michael Hudson
rename some things, change History to be more conventionally constructed
220
    """Decorate a branch to provide information for rendering.
221
222
    History objects are expected to be short lived -- when serving a request
223
    for a particular branch, open it, read-lock it, wrap a History object
224
    around it, serve the request, throw the History object away, unlock the
225
    branch and throw it away.
179.1.12 by Michael Hudson
less repeated locking
226
332.2.8 by Michael Hudson
docstrings (omg!!)
227
    :ivar _file_change_cache: An object that caches information about the
228
        files that changed between two revisions.
229
    :ivar _rev_info: A list of information about revisions.  This is by far
230
        the most cryptic data structure in loggerhead.  At the top level, it
231
        is a list of 3-tuples [(merge-info, where-merged, parents)].
232
        `merge-info` is (seq, revid, merge_depth, revno_str, end_of_merge) --
233
        like a merged sorted list, but the revno is stringified.
234
        `where-merged` is a tuple of revisions that have this revision as a
235
        non-lefthand parent.  Finally, `parents` is just the usual list of
236
        parents of this revision.
237
    :ivar _rev_indices: A dictionary mapping each revision id to the index of
238
        the information about it in _rev_info.
239
    :ivar _full_history: A list of all revision ids in the ancestry of the
240
        branch, in merge-sorted order.  This is a bit silly, and shouldn't
241
        really be stored on the instance...
242
    :ivar _revno_revid: A dictionary mapping stringified revnos to revision
243
        ids.
179.1.10 by Michael Hudson
rename some things, change History to be more conventionally constructed
244
    """
245
332.2.5 by Michael Hudson
refactor in the direction of maybe making sense one day
246
    def _load_whole_history_data(self, caches, cache_key):
332.2.8 by Michael Hudson
docstrings (omg!!)
247
        """Set the attributes relating to the whole history of the branch.
248
249
        :param caches: a list of caches with interfaces like
250
            `RevInfoMemoryCache` and be ordered from fastest to slowest.
251
        :param cache_key: the key to use with the caches.
252
        """
332.2.5 by Michael Hudson
refactor in the direction of maybe making sense one day
253
        self._rev_indices = None
254
        self._rev_info = None
255
256
        missed_caches = []
257
        def update_missed_caches():
258
            for cache in missed_caches:
259
                cache.set(cache_key, self.last_revid, self._rev_info)
260
        for cache in caches:
261
            data = cache.get(cache_key, self.last_revid)
262
            if data is not None:
263
                self._rev_info = data
264
                update_missed_caches()
265
                break
266
            else:
267
                missed_caches.append(cache)
268
        else:
269
            whole_history_data = compute_whole_history_data(self._branch)
270
            self._rev_info, self._rev_indices = whole_history_data
271
            update_missed_caches()
272
273
        if self._rev_indices is not None:
274
            self._full_history = []
275
            self._revno_revid = {}
276
            for ((_, revid, _, revno_str, _), _, _) in self._rev_info:
277
                self._revno_revid[revno_str] = revid
278
                self._full_history.append(revid)
279
        else:
280
            self._full_history = []
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
                self._full_history.append(revid)
287
332.2.3 by Michael Hudson
fairly tortuous two level caching
288
    def __init__(self, branch, whole_history_data_cache, file_cache=None,
332.2.5 by Michael Hudson
refactor in the direction of maybe making sense one day
289
                 revinfo_disk_cache=None, cache_key=None):
179.1.10 by Michael Hudson
rename some things, change History to be more conventionally constructed
290
        assert branch.is_locked(), (
291
            "Can only construct a History object with a read-locked branch.")
332.2.3 by Michael Hudson
fairly tortuous two level caching
292
        if file_cache is not None:
293
            self._file_change_cache = file_cache
294
            file_cache.history = self
295
        else:
296
            self._file_change_cache = None
1 by Robey Pointer
initial checkin
297
        self._branch = branch
201.2.13 by Michael Hudson
have get_inventory cache the inventories
298
        self._inventory_cache = {}
247.1.1 by Martin Albisetti
Change API used to get branch nicks so they don't access the network
299
        self._branch_nick = self._branch.get_config().get_nickname()
300
        self.log = logging.getLogger('loggerhead.%s' % self._branch_nick)
179.1.1 by Michael Hudson
dearie me, all tests pass already!
301
179.1.11 by Michael Hudson
more tidying
302
        self.last_revid = branch.last_revision()
303
332.2.5 by Michael Hudson
refactor in the direction of maybe making sense one day
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)
128.1.46 by Michael Hudson
delete-trailing-whitespace
308
144.1.1 by Michael Hudson
reduce the failures
309
    @property
310
    def has_revisions(self):
311
        return not bzrlib.revision.is_null(self.last_revid)
312
74 by Robey Pointer
add the ability to auto-publish anything found under a particular folder.
313
    def get_config(self):
314
        return self._branch.get_config()
128.1.46 by Michael Hudson
delete-trailing-whitespace
315
1 by Robey Pointer
initial checkin
316
    def get_revno(self, revid):
332.1.1 by Michael Hudson
eliminate some of the more egregious memory usage.
317
        if revid not in self._rev_indices:
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
318
            # ghost parent?
319
            return 'unknown'
332.1.1 by Michael Hudson
eliminate some of the more egregious memory usage.
320
        seq = self._rev_indices[revid]
321
        revno = self._rev_info[seq][0][3]
322
        return revno
1 by Robey Pointer
initial checkin
323
151.1.3 by Michael Hudson
when filtering on a file_id, show the mainline (relative to the start_revid
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 = self._full_history
331
        revid_set = set(revid_list)
332
        revid = start_revid
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
333
151.1.3 by Michael Hudson
when filtering on a file_id, show the mainline (relative to the start_revid
334
        def introduced_revisions(revid):
335
            r = set([revid])
332.1.1 by Michael Hudson
eliminate some of the more egregious memory usage.
336
            seq = self._rev_indices[revid]
337
            md = self._rev_info[seq][0][2]
151.1.3 by Michael Hudson
when filtering on a file_id, show the mainline (relative to the start_revid
338
            i = seq + 1
332.1.1 by Michael Hudson
eliminate some of the more egregious memory usage.
339
            while i < len(self._rev_info) and self._rev_info[i][0][2] > md:
340
                r.add(self._rev_info[i][0][1])
151.1.3 by Michael Hudson
when filtering on a file_id, show the mainline (relative to the start_revid
341
                i += 1
342
            return r
343
        while 1:
151.1.7 by Michael Hudson
empty history fix
344
            if bzrlib.revision.is_null(revid):
345
                return
151.1.3 by Michael Hudson
when filtering on a file_id, show the mainline (relative to the start_revid
346
            if introduced_revisions(revid) & revid_set:
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
347
                yield revid
332.1.1 by Michael Hudson
eliminate some of the more egregious memory usage.
348
            parents = self._rev_info[self._rev_indices[revid]][2]
1 by Robey Pointer
initial checkin
349
            if len(parents) == 0:
350
                return
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
351
            revid = parents[0]
128.1.46 by Michael Hudson
delete-trailing-whitespace
352
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
353
    def get_short_revision_history_by_fileid(self, file_id):
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
354
        # FIXME: would be awesome if we could get, for a folder, the list of
201.1.1 by Martin Albisetti
Added both methods to test API break in 1.6. Bug #253520
355
        # revisions where items within that folder changed.i
356
        try:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
357
            # FIXME: Workaround for bzr versions prior to 1.6b3.
201.1.1 by Martin Albisetti
Added both methods to test API break in 1.6. Bug #253520
358
            # Remove me eventually pretty please  :)
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
359
            w = self._branch.repository.weave_store.get_weave(
360
                    file_id, self._branch.repository.get_transaction())
361
            w_revids = w.versions()
362
            revids = [r for r in self._full_history if r in w_revids]
201.1.1 by Martin Albisetti
Added both methods to test API break in 1.6. Bug #253520
363
        except AttributeError:
364
            possible_keys = [(file_id, revid) for revid in self._full_history]
250.1.1 by James Westby
Chunk the requests to get_parent_map when finding which revisions touch a file.
365
            get_parent_map = self._branch.repository.texts.get_parent_map
366
            # We chunk the requests as this works better with GraphIndex.
367
            # See _filter_revisions_touching_file_id in bzrlib/log.py
368
            # for more information.
369
            revids = []
370
            chunk_size = 1000
371
            for start in xrange(0, len(possible_keys), chunk_size):
372
                next_keys = possible_keys[start:start + chunk_size]
373
                revids += [k[1] for k in get_parent_map(next_keys)]
374
            del possible_keys, next_keys
201.1.1 by Martin Albisetti
Added both methods to test API break in 1.6. Bug #253520
375
        return revids
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
376
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
377
    def get_revision_history_since(self, revid_list, date):
378
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
379
        # so start at midnight on 02-sep.
380
        date = date + datetime.timedelta(days=1)
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
381
        # our revid list is sorted in REVERSE date order,
382
        # so go thru some hoops here...
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
383
        revid_list.reverse()
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
384
        index = bisect.bisect(_RevListToTimestamps(revid_list,
385
                                                   self._branch.repository),
386
                              date)
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
387
        if index == 0:
388
            return []
389
        revid_list.reverse()
390
        index = -index
391
        return revid_list[index:]
128.1.46 by Michael Hudson
delete-trailing-whitespace
392
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
393
    def get_search_revid_list(self, query, revid_list):
394
        """
395
        given a "quick-search" query, try a few obvious possible meanings:
128.1.46 by Michael Hudson
delete-trailing-whitespace
396
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
397
            - revision id or # ("128.1.3")
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
398
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or \
399
iso style "yyyy-mm-dd")
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
400
            - comment text as a fallback
401
402
        and return a revid list that matches.
403
        """
404
        # FIXME: there is some silliness in this action.  we have to look up
405
        # all the relevant changes (time-consuming) only to return a list of
406
        # revids which will be used to fetch a set of changes again.
128.1.46 by Michael Hudson
delete-trailing-whitespace
407
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
408
        # if they entered a revid, just jump straight there;
409
        # ignore the passed-in revid_list
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
410
        revid = self.fix_revid(query)
411
        if revid is not None:
128.4.4 by Michael Hudson
use the sqlite not-shelf for the change cache too
412
            if isinstance(revid, unicode):
413
                revid = revid.encode('utf-8')
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
414
            changes = self.get_changes([revid])
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
415
            if (changes is not None) and (len(changes) > 0):
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
416
                return [revid]
128.1.46 by Michael Hudson
delete-trailing-whitespace
417
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
418
        date = None
419
        m = self.us_date_re.match(query)
420
        if m is not None:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
421
            date = datetime.datetime(util.fix_year(int(m.group(3))),
422
                                     int(m.group(1)),
423
                                     int(m.group(2)))
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
424
        else:
425
            m = self.earth_date_re.match(query)
426
            if m is not None:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
427
                date = datetime.datetime(util.fix_year(int(m.group(3))),
428
                                         int(m.group(2)),
429
                                         int(m.group(1)))
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
430
            else:
431
                m = self.iso_date_re.match(query)
432
                if m is not None:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
433
                    date = datetime.datetime(util.fix_year(int(m.group(1))),
434
                                             int(m.group(2)),
435
                                             int(m.group(3)))
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
436
        if date is not None:
437
            if revid_list is None:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
438
                # if no limit to the query was given,
439
                # search only the direct-parent path.
179.1.11 by Michael Hudson
more tidying
440
                revid_list = list(self.get_revids_from(None, self.last_revid))
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
441
            return self.get_revision_history_since(revid_list, date)
128.1.46 by Michael Hudson
delete-trailing-whitespace
442
23 by Robey Pointer
lots of little changes:
443
    revno_re = re.compile(r'^[\d\.]+$')
47 by Robey Pointer
slowly moving the branch-specific stuff into a common structure...
444
    # the date regex are without a final '$' so that queries like
445
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
446
    # them 90% of what they want instead of nothing at all.)
447
    us_date_re = re.compile(r'^(\d{1,2})/(\d{1,2})/(\d\d(\d\d?))')
448
    earth_date_re = re.compile(r'^(\d{1,2})-(\d{1,2})-(\d\d(\d\d?))')
449
    iso_date_re = re.compile(r'^(\d\d\d\d)-(\d\d)-(\d\d)')
23 by Robey Pointer
lots of little changes:
450
451
    def fix_revid(self, revid):
452
        # if a "revid" is actually a dotted revno, convert it to a revid
453
        if revid is None:
454
            return revid
126 by Robey Pointer
bug 98826: allow "head:" to be used as a valid revid to represent the current
455
        if revid == 'head:':
179.1.11 by Michael Hudson
more tidying
456
            return self.last_revid
217.1.11 by Guillermo Gonzalez
* History.fix_revid raise bzrlib.errors.NoSuchRevision, instead of KeyError
457
        try:
458
            if self.revno_re.match(revid):
459
                revid = self._revno_revid[revid]
460
        except KeyError:
247.1.1 by Martin Albisetti
Change API used to get branch nicks so they don't access the network
461
            raise bzrlib.errors.NoSuchRevision(self._branch_nick, revid)
23 by Robey Pointer
lots of little changes:
462
        return revid
128.1.46 by Michael Hudson
delete-trailing-whitespace
463
43 by Robey Pointer
fix up the other (non-changelog) pages to work with search queries by
464
    def get_file_view(self, revid, file_id):
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
465
        """
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
466
        Given a revid and optional path, return a (revlist, revid) for
467
        navigation through the current scope: from the revid (or the latest
468
        revision) back to the original revision.
128.1.46 by Michael Hudson
delete-trailing-whitespace
469
38 by Robey Pointer
another pile of semi-related changes:
470
        If file_id is None, the entire revision history is the list scope.
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
471
        """
17 by Robey Pointer
think harder about revision traversal, and make it work more deterministic-
472
        if revid is None:
179.1.11 by Michael Hudson
more tidying
473
            revid = self.last_revid
38 by Robey Pointer
another pile of semi-related changes:
474
        if file_id is not None:
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
475
            # since revid is 'start_revid', possibly should start the path
476
            # tracing from revid... FIXME
38 by Robey Pointer
another pile of semi-related changes:
477
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
25 by Robey Pointer
fix a couple of bugs:
478
            revlist = list(self.get_revids_from(revlist, revid))
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
479
        else:
17 by Robey Pointer
think harder about revision traversal, and make it work more deterministic-
480
            revlist = list(self.get_revids_from(None, revid))
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
481
        return revlist
128.1.46 by Michael Hudson
delete-trailing-whitespace
482
42 by Robey Pointer
add text substring indexer
483
    def get_view(self, revid, start_revid, file_id, query=None):
484
        """
485
        use the URL parameters (revid, start_revid, file_id, and query) to
486
        determine the revision list we're viewing (start_revid, file_id, query)
487
        and where we are in it (revid).
128.6.16 by Michael Hudson
d-t-w in history.py
488
128.2.24 by Robey Pointer
clean up epydoc for history.get_view().
489
            - if a query is given, we're viewing query results.
490
            - if a file_id is given, we're viewing revisions for a specific
491
              file.
492
            - if a start_revid is given, we're viewing the branch from a
493
              specific revision up the tree.
494
495
        these may be combined to view revisions for a specific file, from
496
        a specific revision, with a specific search query.
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
497
128.2.24 by Robey Pointer
clean up epydoc for history.get_view().
498
        returns a new (revid, start_revid, revid_list) where:
128.6.16 by Michael Hudson
d-t-w in history.py
499
42 by Robey Pointer
add text substring indexer
500
            - revid: current position within the view
501
            - start_revid: starting revision of this view
502
            - revid_list: list of revision ids for this view
128.1.46 by Michael Hudson
delete-trailing-whitespace
503
42 by Robey Pointer
add text substring indexer
504
        file_id and query are never changed so aren't returned, but they may
505
        contain vital context for future url navigation.
506
        """
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
507
        if start_revid is None:
179.1.11 by Michael Hudson
more tidying
508
            start_revid = self.last_revid
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
509
42 by Robey Pointer
add text substring indexer
510
        if query is None:
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
511
            revid_list = self.get_file_view(start_revid, file_id)
42 by Robey Pointer
add text substring indexer
512
            if revid is None:
513
                revid = start_revid
514
            if revid not in revid_list:
515
                # if the given revid is not in the revlist, use a revlist that
516
                # starts at the given revid.
154.1.1 by Michael Hudson
make daemonization less insane
517
                revid_list = self.get_file_view(revid, file_id)
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
518
                start_revid = revid
43 by Robey Pointer
fix up the other (non-changelog) pages to work with search queries by
519
            return revid, start_revid, revid_list
128.1.46 by Michael Hudson
delete-trailing-whitespace
520
42 by Robey Pointer
add text substring indexer
521
        # potentially limit the search
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
522
        if file_id is not None:
523
            revid_list = self.get_file_view(start_revid, file_id)
42 by Robey Pointer
add text substring indexer
524
        else:
525
            revid_list = None
128.10.14 by Martin Albisetti
* Fix searching
526
        revid_list = search.search_revisions(self._branch, query)
159.1.5 by Michael Hudson
restore fix_year, get rid of try:except:
527
        if revid_list and len(revid_list) > 0:
528
            if revid not in revid_list:
529
                revid = revid_list[0]
530
            return revid, start_revid, revid_list
531
        else:
128.12.1 by Robert Collins
Make bzr-search be an optional dependency and avoid errors when there is no search index.
532
            # XXX: This should return a message saying that the search could
533
            # not be completed due to either missing the plugin or missing a
534
            # search index.
42 by Robey Pointer
add text substring indexer
535
            return None, None, []
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
536
537
    def get_inventory(self, revid):
201.2.13 by Michael Hudson
have get_inventory cache the inventories
538
        if revid not in self._inventory_cache:
539
            self._inventory_cache[revid] = (
540
                self._branch.repository.get_revision_inventory(revid))
541
        return self._inventory_cache[revid]
1 by Robey Pointer
initial checkin
542
38 by Robey Pointer
another pile of semi-related changes:
543
    def get_path(self, revid, file_id):
544
        if (file_id is None) or (file_id == ''):
545
            return ''
201.2.12 by Michael Hudson
direct all calls to get_revision_inventory through History.get_inventory
546
        path = self.get_inventory(revid).id2path(file_id)
38 by Robey Pointer
another pile of semi-related changes:
547
        if (len(path) > 0) and not path.startswith('/'):
548
            path = '/' + path
549
        return path
128.1.46 by Michael Hudson
delete-trailing-whitespace
550
125 by Robey Pointer
bug 98826: allow 'annotate' to take a path on the URL line instead of a
551
    def get_file_id(self, revid, path):
552
        if (len(path) > 0) and not path.startswith('/'):
553
            path = '/' + path
201.2.12 by Michael Hudson
direct all calls to get_revision_inventory through History.get_inventory
554
        return self.get_inventory(revid).path2id(path)
128.1.46 by Michael Hudson
delete-trailing-whitespace
555
1 by Robey Pointer
initial checkin
556
    def get_merge_point_list(self, revid):
557
        """
558
        Return the list of revids that have merged this node.
559
        """
128.1.21 by Michael Hudson
kill more dead code (this might help startup time a bit, even)
560
        if '.' not in self.get_revno(revid):
1 by Robey Pointer
initial checkin
561
            return []
128.1.46 by Michael Hudson
delete-trailing-whitespace
562
1 by Robey Pointer
initial checkin
563
        merge_point = []
564
        while True:
332.1.1 by Michael Hudson
eliminate some of the more egregious memory usage.
565
            children = self._rev_info[self._rev_indices[revid]][1]
1 by Robey Pointer
initial checkin
566
            nexts = []
567
            for child in children:
332.1.1 by Michael Hudson
eliminate some of the more egregious memory usage.
568
                child_parents = self._rev_info[self._rev_indices[child]][2]
1 by Robey Pointer
initial checkin
569
                if child_parents[0] == revid:
570
                    nexts.append(child)
571
                else:
572
                    merge_point.append(child)
573
574
            if len(nexts) == 0:
575
                # only merge
576
                return merge_point
577
578
            while len(nexts) > 1:
579
                # branch
580
                next = nexts.pop()
581
                merge_point_next = self.get_merge_point_list(next)
582
                merge_point.extend(merge_point_next)
583
584
            revid = nexts[0]
128.1.46 by Michael Hudson
delete-trailing-whitespace
585
1 by Robey Pointer
initial checkin
586
    def simplify_merge_point_list(self, revids):
587
        """if a revision is already merged, don't show further merge points"""
588
        d = {}
589
        for revid in revids:
590
            revno = self.get_revno(revid)
591
            revnol = revno.split(".")
592
            revnos = ".".join(revnol[:-2])
593
            revnolast = int(revnol[-1])
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
594
            if revnos in d.keys():
1 by Robey Pointer
initial checkin
595
                m = d[revnos][0]
596
                if revnolast < m:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
597
                    d[revnos] = (revnolast, revid)
1 by Robey Pointer
initial checkin
598
            else:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
599
                d[revnos] = (revnolast, revid)
1 by Robey Pointer
initial checkin
600
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
601
        return [d[revnos][1] for revnos in d.keys()]
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
602
305.1.13 by Michael Hudson
* actually finish getting file_changes of the change object.
603
    def add_branch_nicks(self, change):
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
604
        """
305.1.13 by Michael Hudson
* actually finish getting file_changes of the change object.
605
        given a 'change', fill in the branch nicks on all parents and merge
606
        points.
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
607
        """
608
        fetch_set = set()
305.1.13 by Michael Hudson
* actually finish getting file_changes of the change object.
609
        for p in change.parents:
610
            fetch_set.add(p.revid)
611
        for p in change.merge_points:
612
            fetch_set.add(p.revid)
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
613
        p_changes = self.get_changes(list(fetch_set))
614
        p_change_dict = dict([(c.revid, c) for c in p_changes])
305.1.13 by Michael Hudson
* actually finish getting file_changes of the change object.
615
        for p in change.parents:
616
            if p.revid in p_change_dict:
617
                p.branch_nick = p_change_dict[p.revid].branch_nick
618
            else:
619
                p.branch_nick = '(missing)'
620
        for p in change.merge_points:
621
            if p.revid in p_change_dict:
622
                p.branch_nick = p_change_dict[p.revid].branch_nick
623
            else:
624
                p.branch_nick = '(missing)'
128.1.46 by Michael Hudson
delete-trailing-whitespace
625
128.1.47 by Michael Hudson
kill the get_diffs argument to get_changes entirely.
626
    def get_changes(self, revid_list):
144.1.7 by Michael Hudson
add just one docstring
627
        """Return a list of changes objects for the given revids.
628
629
        Revisions not present and NULL_REVISION will be ignored.
630
        """
128.11.1 by Martin Albisetti
* Remove text index caching
631
        changes = self.get_changes_uncached(revid_list)
128.1.15 by Michael Hudson
fix bug #92435, but by hacking not by understanding
632
        if len(changes) == 0:
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
633
            return changes
128.1.46 by Michael Hudson
delete-trailing-whitespace
634
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
635
        # some data needs to be recalculated each time, because it may
636
        # change as new revisions are added.
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
637
        for change in changes:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
638
            merge_revids = self.simplify_merge_point_list(
639
                               self.get_merge_point_list(change.revid))
640
            change.merge_points = [
641
                util.Container(revid=r,
642
                revno=self.get_revno(r)) for r in merge_revids]
128.2.20 by Robey Pointer
unfiled bug that i just noticed today:
643
            if len(change.parents) > 0:
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
644
                change.parents = [util.Container(revid=r,
128.11.1 by Martin Albisetti
* Remove text index caching
645
                    revno=self.get_revno(r)) for r in change.parents]
128.2.12 by Robey Pointer
don't save the revno in the revision cache, because if two branches use
646
            change.revno = self.get_revno(change.revid)
113.1.3 by Kent Gibson
Rework changes page to provide single line summary per change.
647
648
        parity = 0
649
        for change in changes:
650
            change.parity = parity
651
            parity ^= 1
128.1.46 by Michael Hudson
delete-trailing-whitespace
652
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
653
        return changes
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
654
128.2.18 by Robey Pointer
rename get_diff to get_change_relative_to (since it returns the entire
655
    def get_changes_uncached(self, revid_list):
128.8.1 by Martin Albisetti
Merge in changes with trunk
656
        # FIXME: deprecated method in getting a null revision
144.1.3 by Michael Hudson
fix two more tests
657
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
658
                            revid_list)
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
659
        parent_map = self._branch.repository.get_graph().get_parent_map(
660
                         revid_list)
148.1.3 by Michael Hudson
use a list comprehension instead
661
        # We need to return the answer in the same order as the input,
662
        # less any ghosts.
663
        present_revids = [revid for revid in revid_list
664
                          if revid in parent_map]
128.11.1 by Martin Albisetti
* Remove text index caching
665
        rev_list = self._branch.repository.get_revisions(present_revids)
128.2.31 by Robey Pointer
fix bugs introduced by incorrectly resolving the previous conflicts. (i
666
144.1.2 by Michael Hudson
use less dumb method of filtering revisions
667
        return [self._change_from_revision(rev) for rev in rev_list]
128.1.46 by Michael Hudson
delete-trailing-whitespace
668
128.2.18 by Robey Pointer
rename get_diff to get_change_relative_to (since it returns the entire
669
    def _change_from_revision(self, revision):
670
        """
671
        Given a bzrlib Revision, return a processed "change" for use in
672
        templates.
673
        """
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
674
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
128.1.46 by Michael Hudson
delete-trailing-whitespace
675
230.1.1 by Steve 'Ashcrow' Milner
Updated to follow pep8.
676
        parents = [util.Container(revid=r,
677
                   revno=self.get_revno(r)) for r in revision.parent_ids]
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
678
679
        message, short_message = clean_message(revision.message)
680
294.3.1 by Matt Nordhoff
Use revision.get_apparent_authors() when available, and support displaying multiple authors for a revision.
681
        try:
682
            authors = revision.get_apparent_authors()
683
        except AttributeError:
684
            authors = [revision.get_apparent_author()]
685
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
686
        entry = {
687
            'revid': revision.revision_id,
688
            'date': commit_time,
294.3.1 by Matt Nordhoff
Use revision.get_apparent_authors() when available, and support displaying multiple authors for a revision.
689
            'authors': authors,
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
690
            'branch_nick': revision.properties.get('branch-nick', None),
691
            'short_comment': short_message,
692
            'comment': revision.message,
693
            'comment_clean': [util.html_clean(s) for s in message],
128.2.20 by Robey Pointer
unfiled bug that i just noticed today:
694
            'parents': revision.parent_ids,
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
695
        }
696
        return util.Container(entry)
697
305.1.5 by Michael Hudson
ooh nice, remove gobs of code. me happy
698
    def get_file_changes_uncached(self, entry):
699
        repo = self._branch.repository
700
        if entry.parents:
308.1.3 by Michael Hudson
push all handling of revision trees into history
701
            old_revid = entry.parents[0].revid
305.1.5 by Michael Hudson
ooh nice, remove gobs of code. me happy
702
        else:
308.1.3 by Michael Hudson
push all handling of revision trees into history
703
            old_revid = bzrlib.revision.NULL_REVISION
308.1.4 by Michael Hudson
allow common-case revision page to get its information from the cache
704
        return self.file_changes_for_revision_ids(old_revid, entry.revid)
305.1.5 by Michael Hudson
ooh nice, remove gobs of code. me happy
705
706
    def get_file_changes(self, entry):
128.1.55 by Michael Hudson
plumbing for a file change cache
707
        if self._file_change_cache is None:
305.1.5 by Michael Hudson
ooh nice, remove gobs of code. me happy
708
            return self.get_file_changes_uncached(entry)
128.1.55 by Michael Hudson
plumbing for a file change cache
709
        else:
305.1.5 by Michael Hudson
ooh nice, remove gobs of code. me happy
710
            return self._file_change_cache.get_file_changes(entry)
128.1.52 by Michael Hudson
random tidying towards being able to add caching of changed file lists
711
305.1.4 by Michael Hudson
being outside in simplification of interface
712
    def add_changes(self, entry):
305.1.5 by Michael Hudson
ooh nice, remove gobs of code. me happy
713
        changes = self.get_file_changes(entry)
305.1.4 by Michael Hudson
being outside in simplification of interface
714
        entry.changes = changes
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
715
38 by Robey Pointer
another pile of semi-related changes:
716
    def get_file(self, file_id, revid):
106 by Robey Pointer
oops, fix up the download logging for real.
717
        "returns (path, filename, data)"
718
        inv = self.get_inventory(revid)
719
        inv_entry = inv[file_id]
37 by Robey Pointer
don't bother to rebuild the cache when it's full
720
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
106 by Robey Pointer
oops, fix up the download logging for real.
721
        path = inv.id2path(file_id)
722
        if not path.startswith('/'):
723
            path = '/' + path
724
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
128.1.46 by Michael Hudson
delete-trailing-whitespace
725
308.1.4 by Michael Hudson
allow common-case revision page to get its information from the cache
726
    def file_changes_for_revision_ids(self, old_revid, new_revid):
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
727
        """
728
        Return a nested data structure containing the changes in a delta::
128.1.46 by Michael Hudson
delete-trailing-whitespace
729
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
730
            added: list((filename, file_id)),
731
            renamed: list((old_filename, new_filename, file_id)),
732
            deleted: list((filename, file_id)),
733
            modified: list(
734
                filename: str,
735
                file_id: str,
305.1.3 by Michael Hudson
Less repitition in the delta handling code and less tangled
736
            ),
737
            text_changes: list((filename, file_id)),
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
738
        """
308.1.7 by Michael Hudson
work for the initial revision
739
        repo = self._branch.repository
740
        if bzrlib.revision.is_null(old_revid) or \
741
               bzrlib.revision.is_null(new_revid):
742
            old_tree, new_tree = map(
743
                repo.revision_tree, [old_revid, new_revid])
744
        else:
745
            old_tree, new_tree = repo.revision_trees([old_revid, new_revid])
308.1.3 by Michael Hudson
push all handling of revision trees into history
746
747
        reporter = FileChangeReporter(old_tree.inventory, new_tree.inventory)
305.1.8 by Michael Hudson
finally, use iter_changes and report_changes, not changes_from.
748
749
        bzrlib.delta.report_changes(new_tree.iter_changes(old_tree), reporter)
305.1.3 by Michael Hudson
Less repitition in the delta handling code and less tangled
750
751
        return util.Container(
305.1.8 by Michael Hudson
finally, use iter_changes and report_changes, not changes_from.
752
            added=sorted(reporter.added, key=lambda x:x.filename),
753
            renamed=sorted(reporter.renamed, key=lambda x:x.new_filename),
754
            removed=sorted(reporter.removed, key=lambda x:x.filename),
755
            modified=sorted(reporter.modified, key=lambda x:x.filename),
305.1.14 by Michael Hudson
oops
756
            text_changes=sorted(reporter.text_changes, key=lambda x:x.filename))