~loggerhead-team/loggerhead/trunk

1 by Robey Pointer
initial checkin
1
#
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
23 by Robey Pointer
lots of little changes:
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
48 by Robey Pointer
the big migration of branch-specific data to a BranchView object: actually
4
# Copyright (C) 2005  Jake Edge <jake@edge2.net>
5
# Copyright (C) 2005  Matt Mackall <mpm@selenic.com>
1 by Robey Pointer
initial checkin
6
#
7
# This program is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 2 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program; if not, write to the Free Software
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
#
21
48 by Robey Pointer
the big migration of branch-specific data to a BranchView object: actually
22
#
23
# This file (and many of the web templates) contains work based on the
24
# "bazaar-webserve" project by Goffredo Baroncelli, which is in turn based
25
# on "hgweb" by Jake Edge and Matt Mackall.
26
#
27
28
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
29
import bisect
1 by Robey Pointer
initial checkin
30
import datetime
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
31
import logging
23 by Robey Pointer
lots of little changes:
32
import re
1 by Robey Pointer
initial checkin
33
import textwrap
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
34
import threading
35
import time
2 by Robey Pointer
add revision page, put some of the config in the config file
36
from StringIO import StringIO
1 by Robey Pointer
initial checkin
37
24 by Robey Pointer
figured out how to make my own separate config file like BzrInspect, and
38
from loggerhead import util
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
39
from loggerhead.util import decorator
40
1 by Robey Pointer
initial checkin
41
import bzrlib
42
import bzrlib.branch
2 by Robey Pointer
add revision page, put some of the config in the config file
43
import bzrlib.diff
1 by Robey Pointer
initial checkin
44
import bzrlib.errors
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
45
import bzrlib.progress
128.1.60 by Michael Hudson
respond to review comments
46
import bzrlib.revision
1 by Robey Pointer
initial checkin
47
import bzrlib.tsort
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
48
import bzrlib.ui
1 by Robey Pointer
initial checkin
49
156.1.7 by Michael Hudson
trivial whitespace issues
50
49 by Robey Pointer
add top-level page listing available branches. also a patch from matty to not require external-url in atom feeds any more
51
with_branch_lock = util.with_lock('_lock', 'branch')
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
52
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
53
54
@decorator
55
def with_bzrlib_read_lock(unbound):
56
    def bzrlib_read_locked(self, *args, **kw):
49 by Robey Pointer
add top-level page listing available branches. also a patch from matty to not require external-url in atom feeds any more
57
        #self.log.debug('-> %r bzr lock', id(threading.currentThread()))
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
58
        self._branch.repository.lock_read()
59
        try:
60
            return unbound(self, *args, **kw)
61
        finally:
62
            self._branch.repository.unlock()
49 by Robey Pointer
add top-level page listing available branches. also a patch from matty to not require external-url in atom feeds any more
63
            #self.log.debug('<- %r bzr lock', id(threading.currentThread()))
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
64
    return bzrlib_read_locked
65
66
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
67
# bzrlib's UIFactory is not thread-safe
68
uihack = threading.local()
69
70
class ThreadSafeUIFactory (bzrlib.ui.SilentUIFactory):
71
    def nested_progress_bar(self):
72
        if getattr(uihack, '_progress_bar_stack', None) is None:
73
            uihack._progress_bar_stack = bzrlib.progress.ProgressBarStack(klass=bzrlib.progress.DummyProgress)
74
        return uihack._progress_bar_stack.get_nested()
75
76
bzrlib.ui.ui_factory = ThreadSafeUIFactory()
77
78
66 by Robey Pointer
do side-by-side diff on the revision page, making it the default.
79
def _process_side_by_side_buffers(line_list, delete_list, insert_list):
80
    while len(delete_list) < len(insert_list):
81
        delete_list.append((None, '', 'context'))
82
    while len(insert_list) < len(delete_list):
83
        insert_list.append((None, '', 'context'))
84
    while len(delete_list) > 0:
85
        d = delete_list.pop(0)
86
        i = insert_list.pop(0)
87
        line_list.append(util.Container(old_lineno=d[0], new_lineno=i[0],
88
                                        old_line=d[1], new_line=i[1],
89
                                        old_type=d[2], new_type=i[2]))
90
91
92
def _make_side_by_side(chunk_list):
93
    """
94
    turn a normal unified-style diff (post-processed by parse_delta) into a
95
    side-by-side diff structure.  the new structure is::
128.1.46 by Michael Hudson
delete-trailing-whitespace
96
66 by Robey Pointer
do side-by-side diff on the revision page, making it the default.
97
        chunks: list(
98
            diff: list(
99
                old_lineno: int,
100
                new_lineno: int,
101
                old_line: str,
102
                new_line: str,
103
                type: str('context' or 'changed'),
104
            )
105
        )
106
    """
107
    out_chunk_list = []
108
    for chunk in chunk_list:
109
        line_list = []
110
        delete_list, insert_list = [], []
111
        for line in chunk.diff:
112
            if line.type == 'context':
113
                if len(delete_list) or len(insert_list):
114
                    _process_side_by_side_buffers(line_list, delete_list, insert_list)
115
                    delete_list, insert_list = [], []
116
                line_list.append(util.Container(old_lineno=line.old_lineno, new_lineno=line.new_lineno,
117
                                                old_line=line.line, new_line=line.line,
118
                                                old_type=line.type, new_type=line.type))
119
            elif line.type == 'delete':
120
                delete_list.append((line.old_lineno, line.line, line.type))
121
            elif line.type == 'insert':
122
                insert_list.append((line.new_lineno, line.line, line.type))
123
        if len(delete_list) or len(insert_list):
124
            _process_side_by_side_buffers(line_list, delete_list, insert_list)
125
        out_chunk_list.append(util.Container(diff=line_list))
126
    return out_chunk_list
127
128
74 by Robey Pointer
add the ability to auto-publish anything found under a particular folder.
129
def is_branch(folder):
130
    try:
131
        bzrlib.branch.Branch.open(folder)
132
        return True
133
    except:
134
        return False
135
136
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
137
def clean_message(message):
138.1.3 by Michael Hudson
clean up various comments and add docstrings
138
    """Clean up a commit message and return it and a short (1-line) version.
139
140
    Commit messages that are long single lines are reflowed using the textwrap
141
    module (Robey, the original author of this code, apparently favored this
142
    style of message).
143
    """
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
144
    message = message.splitlines()
138.1.3 by Michael Hudson
clean up various comments and add docstrings
145
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
146
    if len(message) == 1:
147
        message = textwrap.wrap(message[0])
138.1.3 by Michael Hudson
clean up various comments and add docstrings
148
138.1.2 by Michael Hudson
test and fix for the problem
149
    if len(message) == 0:
138.1.3 by Michael Hudson
clean up various comments and add docstrings
150
        # We can end up where when (a) the commit message was empty or (b)
151
        # when the message consisted entirely of whitespace, in which case
152
        # textwrap.wrap() returns an empty list.
153
        return [''], ''
128.1.46 by Michael Hudson
delete-trailing-whitespace
154
138.1.3 by Michael Hudson
clean up various comments and add docstrings
155
    # Make short form of commit message.
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
156
    short_message = message[0]
128.6.12 by Michael Hudson
go back to 60 character 'short' commit messages
157
    if len(short_message) > 60:
158
        short_message = short_message[:60] + '...'
128.1.46 by Michael Hudson
delete-trailing-whitespace
159
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
160
    return message, short_message
161
162
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
163
def rich_filename(path, kind):
164
    if kind == 'directory':
165
        path += '/'
166
    if kind == 'symlink':
167
        path += '@'
168
    return path
128.1.46 by Michael Hudson
delete-trailing-whitespace
169
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
170
171
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
172
# from bzrlib
173
class _RevListToTimestamps(object):
174
    """This takes a list of revisions, and allows you to bisect by date"""
175
176
    __slots__ = ['revid_list', 'repository']
177
178
    def __init__(self, revid_list, repository):
179
        self.revid_list = revid_list
180
        self.repository = repository
181
182
    def __getitem__(self, index):
183
        """Get the date of the index'd item"""
184
        return datetime.datetime.fromtimestamp(self.repository.get_revision(self.revid_list[index]).timestamp)
185
186
    def __len__(self):
187
        return len(self.revid_list)
188
128.1.26 by Michael Hudson
same trick for revision.kid. still way too slow for big diffs though
189
1 by Robey Pointer
initial checkin
190
class History (object):
128.1.46 by Michael Hudson
delete-trailing-whitespace
191
1 by Robey Pointer
initial checkin
192
    def __init__(self):
128.1.55 by Michael Hudson
plumbing for a file change cache
193
        self._file_change_cache = None
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
194
        self._lock = threading.RLock()
128.1.46 by Michael Hudson
delete-trailing-whitespace
195
1 by Robey Pointer
initial checkin
196
    @classmethod
159.2.42 by Michael Hudson
make keeping up to date less ugly
197
    def from_branch(cls, branch):
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
198
        z = time.time()
1 by Robey Pointer
initial checkin
199
        self = cls()
200
        self._branch = branch
128.1.21 by Michael Hudson
kill more dead code (this might help startup time a bit, even)
201
        self._last_revid = self._branch.last_revision()
156.1.7 by Michael Hudson
trivial whitespace issues
202
159.2.42 by Michael Hudson
make keeping up to date less ugly
203
        self.log = logging.getLogger('loggerhead.%s' % (self._branch.nick,))
156.1.2 by Martin Albisetti
Cleanup new method to get the revision graph
204
205
        graph = branch.repository.get_graph()
206
        parent_map = dict(((key, value) for key, value in
207
             graph.iter_ancestry([self._last_revid]) if value is not None))
156.1.7 by Michael Hudson
trivial whitespace issues
208
209
        self._revision_graph = self._strip_NULL_ghosts(parent_map)
1 by Robey Pointer
initial checkin
210
        self._full_history = []
211
        self._revision_info = {}
212
        self._revno_revid = {}
144.1.1 by Michael Hudson
reduce the failures
213
        if bzrlib.revision.is_null(self._last_revid):
214
            self._merge_sort = []
215
        else:
216
            self._merge_sort = bzrlib.tsort.merge_sort(
156.1.7 by Michael Hudson
trivial whitespace issues
217
                self._revision_graph, self._last_revid, generate_revno=True)
156.1.1 by Martin Albisetti
Replace deprecated method
218
1 by Robey Pointer
initial checkin
219
        for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
220
            self._full_history.append(revid)
221
            revno_str = '.'.join(str(n) for n in revno)
222
            self._revno_revid[revno_str] = revid
156.1.7 by Michael Hudson
trivial whitespace issues
223
            self._revision_info[revid] = (
224
                seq, revid, merge_depth, revno_str, end_of_merge)
225
1 by Robey Pointer
initial checkin
226
        # cache merge info
227
        self._where_merged = {}
156.1.7 by Michael Hudson
trivial whitespace issues
228
1 by Robey Pointer
initial checkin
229
        for revid in self._revision_graph.keys():
128.5.1 by Aaron Bentley
Optimize History.from_subfolder
230
            if self._revision_info[revid][2] == 0:
1 by Robey Pointer
initial checkin
231
                continue
232
            for parent in self._revision_graph[revid]:
233
                self._where_merged.setdefault(parent, set()).add(revid)
234
47 by Robey Pointer
slowly moving the branch-specific stuff into a common structure...
235
        self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
1 by Robey Pointer
initial checkin
236
        return self
128.1.46 by Michael Hudson
delete-trailing-whitespace
237
156.1.6 by Martin Albisetti
* Copied _strip_NULL_ghosts from bzrlib "temporarily"
238
    @staticmethod
239
    def _strip_NULL_ghosts(revision_graph):
240
        """
241
        Copied over from bzrlib meant as a temporary workaround deprecated 
242
        methods.
243
        """
244
245
        # Filter ghosts, and null:
246
        if bzrlib.revision.NULL_REVISION in revision_graph:
247
            del revision_graph[bzrlib.revision.NULL_REVISION]
248
        for key, parents in revision_graph.items():
249
            revision_graph[key] = tuple(parent for parent in parents if parent
250
                in revision_graph)
251
        return revision_graph
252
1 by Robey Pointer
initial checkin
253
    @classmethod
159.2.42 by Michael Hudson
make keeping up to date less ugly
254
    def from_folder(cls, path):
1 by Robey Pointer
initial checkin
255
        b = bzrlib.branch.Branch.open(path)
128.5.1 by Aaron Bentley
Optimize History.from_subfolder
256
        b.lock_read()
257
        try:
159.2.42 by Michael Hudson
make keeping up to date less ugly
258
            return cls.from_branch(b)
128.5.1 by Aaron Bentley
Optimize History.from_subfolder
259
        finally:
260
            b.unlock()
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
261
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
262
    @with_branch_lock
20 by Robey Pointer
add a timed event to fill in the revision cache, so that after running for
263
    def out_of_date(self):
128.2.10 by Robey Pointer
i believe the sense of this test was backwards
264
        # the branch may have been upgraded on disk, in which case we're stale.
144.1.5 by Michael Hudson
test for repository upgrades too, fixing the last test
265
        newly_opened = bzrlib.branch.Branch.open(self._branch.base)
128.1.24 by Michael Hudson
test and fix for #118673.
266
        if self._branch.__class__ is not \
144.1.5 by Michael Hudson
test for repository upgrades too, fixing the last test
267
               newly_opened.__class__:
268
            return True
269
        if self._branch.repository.__class__ is not \
270
               newly_opened.repository.__class__:
128.2.10 by Robey Pointer
i believe the sense of this test was backwards
271
            return True
128.1.24 by Michael Hudson
test and fix for #118673.
272
        return self._branch.last_revision() != self._last_revid
20 by Robey Pointer
add a timed event to fill in the revision cache, so that after running for
273
128.1.55 by Michael Hudson
plumbing for a file change cache
274
    def use_file_cache(self, cache):
275
        self._file_change_cache = cache
276
144.1.1 by Michael Hudson
reduce the failures
277
    @property
278
    def has_revisions(self):
279
        return not bzrlib.revision.is_null(self.last_revid)
280
1 by Robey Pointer
initial checkin
281
    last_revid = property(lambda self: self._last_revid, None, None)
128.1.46 by Michael Hudson
delete-trailing-whitespace
282
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
283
    @with_branch_lock
74 by Robey Pointer
add the ability to auto-publish anything found under a particular folder.
284
    def get_config(self):
285
        return self._branch.get_config()
128.1.46 by Michael Hudson
delete-trailing-whitespace
286
1 by Robey Pointer
initial checkin
287
    def get_revno(self, revid):
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
288
        if revid not in self._revision_info:
289
            # ghost parent?
290
            return 'unknown'
1 by Robey Pointer
initial checkin
291
        seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
292
        return revno_str
293
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
294
    def get_revision_history(self):
295
        return self._full_history
128.1.46 by Michael Hudson
delete-trailing-whitespace
296
151.1.3 by Michael Hudson
when filtering on a file_id, show the mainline (relative to the start_revid
297
    def get_revids_from(self, revid_list, start_revid):
298
        """
299
        Yield the mainline (wrt start_revid) revisions that merged each
300
        revid in revid_list.
301
        """
302
        if revid_list is None:
303
            revid_list = self._full_history
304
        revid_set = set(revid_list)
305
        revid = start_revid
306
        def introduced_revisions(revid):
307
            r = set([revid])
308
            seq, revid, md, revno, end_of_merge = self._revision_info[revid]
309
            i = seq + 1
310
            while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
311
                r.add(self._merge_sort[i][1])
312
                i += 1
313
            return r
314
        while 1:
151.1.7 by Michael Hudson
empty history fix
315
            if bzrlib.revision.is_null(revid):
316
                return
151.1.3 by Michael Hudson
when filtering on a file_id, show the mainline (relative to the start_revid
317
            if introduced_revisions(revid) & revid_set:
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
318
                yield revid
1 by Robey Pointer
initial checkin
319
            parents = self._revision_graph[revid]
320
            if len(parents) == 0:
321
                return
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
322
            revid = parents[0]
128.1.46 by Michael Hudson
delete-trailing-whitespace
323
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
324
    @with_branch_lock
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
325
    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
326
        # wow.  is this really the only way we can get this list?  by
327
        # man-handling the weave store directly? :-0
328
        # FIXME: would be awesome if we could get, for a folder, the list of
329
        # revisions where items within that folder changed.
330
        w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
331
        w_revids = w.versions()
332
        revids = [r for r in self._full_history if r in w_revids]
333
        return revids
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
334
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
335
    @with_branch_lock
336
    def get_revision_history_since(self, revid_list, date):
337
        # if a user asks for revisions starting at 01-sep, they mean inclusive,
338
        # so start at midnight on 02-sep.
339
        date = date + datetime.timedelta(days=1)
340
        # our revid list is sorted in REVERSE date order, so go thru some hoops here...
341
        revid_list.reverse()
342
        index = bisect.bisect(_RevListToTimestamps(revid_list, self._branch.repository), date)
343
        if index == 0:
344
            return []
345
        revid_list.reverse()
346
        index = -index
347
        return revid_list[index:]
128.1.46 by Michael Hudson
delete-trailing-whitespace
348
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
349
    @with_branch_lock
350
    def get_search_revid_list(self, query, revid_list):
351
        """
352
        given a "quick-search" query, try a few obvious possible meanings:
128.1.46 by Michael Hudson
delete-trailing-whitespace
353
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
354
            - revision id or # ("128.1.3")
355
            - date (US style "mm/dd/yy", earth style "dd-mm-yy", or iso style "yyyy-mm-dd")
356
            - comment text as a fallback
357
358
        and return a revid list that matches.
359
        """
360
        # FIXME: there is some silliness in this action.  we have to look up
361
        # all the relevant changes (time-consuming) only to return a list of
362
        # revids which will be used to fetch a set of changes again.
128.1.46 by Michael Hudson
delete-trailing-whitespace
363
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
364
        # if they entered a revid, just jump straight there; ignore the passed-in revid_list
365
        revid = self.fix_revid(query)
366
        if revid is not None:
128.4.4 by Michael Hudson
use the sqlite not-shelf for the change cache too
367
            if isinstance(revid, unicode):
368
                revid = revid.encode('utf-8')
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
369
            changes = self.get_changes([ revid ])
370
            if (changes is not None) and (len(changes) > 0):
371
                return [ revid ]
128.1.46 by Michael Hudson
delete-trailing-whitespace
372
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
373
        date = None
374
        m = self.us_date_re.match(query)
375
        if m is not None:
376
            date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(1)), int(m.group(2)))
377
        else:
378
            m = self.earth_date_re.match(query)
379
            if m is not None:
380
                date = datetime.datetime(util.fix_year(int(m.group(3))), int(m.group(2)), int(m.group(1)))
381
            else:
382
                m = self.iso_date_re.match(query)
383
                if m is not None:
384
                    date = datetime.datetime(util.fix_year(int(m.group(1))), int(m.group(2)), int(m.group(3)))
385
        if date is not None:
386
            if revid_list is None:
387
                # if no limit to the query was given, search only the direct-parent path.
388
                revid_list = list(self.get_revids_from(None, self._last_revid))
389
            return self.get_revision_history_since(revid_list, date)
128.1.46 by Michael Hudson
delete-trailing-whitespace
390
23 by Robey Pointer
lots of little changes:
391
    revno_re = re.compile(r'^[\d\.]+$')
47 by Robey Pointer
slowly moving the branch-specific stuff into a common structure...
392
    # the date regex are without a final '$' so that queries like
393
    # "2006-11-30 12:15" still mostly work.  (i think it's better to give
394
    # them 90% of what they want instead of nothing at all.)
395
    us_date_re = re.compile(r'^(\d{1,2})/(\d{1,2})/(\d\d(\d\d?))')
396
    earth_date_re = re.compile(r'^(\d{1,2})-(\d{1,2})-(\d\d(\d\d?))')
397
    iso_date_re = re.compile(r'^(\d\d\d\d)-(\d\d)-(\d\d)')
23 by Robey Pointer
lots of little changes:
398
399
    def fix_revid(self, revid):
400
        # if a "revid" is actually a dotted revno, convert it to a revid
401
        if revid is None:
402
            return revid
126 by Robey Pointer
bug 98826: allow "head:" to be used as a valid revid to represent the current
403
        if revid == 'head:':
404
            return self._last_revid
23 by Robey Pointer
lots of little changes:
405
        if self.revno_re.match(revid):
406
            revid = self._revno_revid[revid]
407
        return revid
128.1.46 by Michael Hudson
delete-trailing-whitespace
408
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
409
    @with_branch_lock
43 by Robey Pointer
fix up the other (non-changelog) pages to work with search queries by
410
    def get_file_view(self, revid, file_id):
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
411
        """
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
412
        Given a revid and optional path, return a (revlist, revid) for
413
        navigation through the current scope: from the revid (or the latest
414
        revision) back to the original revision.
128.1.46 by Michael Hudson
delete-trailing-whitespace
415
38 by Robey Pointer
another pile of semi-related changes:
416
        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
417
        """
17 by Robey Pointer
think harder about revision traversal, and make it work more deterministic-
418
        if revid is None:
419
            revid = self._last_revid
38 by Robey Pointer
another pile of semi-related changes:
420
        if file_id is not None:
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
421
            # since revid is 'start_revid', possibly should start the path
422
            # tracing from revid... FIXME
38 by Robey Pointer
another pile of semi-related changes:
423
            revlist = list(self.get_short_revision_history_by_fileid(file_id))
25 by Robey Pointer
fix a couple of bugs:
424
            revlist = list(self.get_revids_from(revlist, revid))
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
425
        else:
17 by Robey Pointer
think harder about revision traversal, and make it work more deterministic-
426
            revlist = list(self.get_revids_from(None, revid))
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
427
        return revlist
128.1.46 by Michael Hudson
delete-trailing-whitespace
428
42 by Robey Pointer
add text substring indexer
429
    @with_branch_lock
430
    def get_view(self, revid, start_revid, file_id, query=None):
431
        """
432
        use the URL parameters (revid, start_revid, file_id, and query) to
433
        determine the revision list we're viewing (start_revid, file_id, query)
434
        and where we are in it (revid).
128.6.16 by Michael Hudson
d-t-w in history.py
435
128.2.24 by Robey Pointer
clean up epydoc for history.get_view().
436
            - if a query is given, we're viewing query results.
437
            - if a file_id is given, we're viewing revisions for a specific
438
              file.
439
            - if a start_revid is given, we're viewing the branch from a
440
              specific revision up the tree.
441
442
        these may be combined to view revisions for a specific file, from
443
        a specific revision, with a specific search query.
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
444
128.2.24 by Robey Pointer
clean up epydoc for history.get_view().
445
        returns a new (revid, start_revid, revid_list) where:
128.6.16 by Michael Hudson
d-t-w in history.py
446
42 by Robey Pointer
add text substring indexer
447
            - revid: current position within the view
448
            - start_revid: starting revision of this view
449
            - revid_list: list of revision ids for this view
128.1.46 by Michael Hudson
delete-trailing-whitespace
450
42 by Robey Pointer
add text substring indexer
451
        file_id and query are never changed so aren't returned, but they may
452
        contain vital context for future url navigation.
453
        """
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
454
        if start_revid is None:
455
            start_revid = self._last_revid
456
42 by Robey Pointer
add text substring indexer
457
        if query is None:
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
458
            revid_list = self.get_file_view(start_revid, file_id)
42 by Robey Pointer
add text substring indexer
459
            if revid is None:
460
                revid = start_revid
461
            if revid not in revid_list:
462
                # if the given revid is not in the revlist, use a revlist that
463
                # starts at the given revid.
154.1.1 by Michael Hudson
make daemonization less insane
464
                revid_list = self.get_file_view(revid, file_id)
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
465
                start_revid = revid
43 by Robey Pointer
fix up the other (non-changelog) pages to work with search queries by
466
            return revid, start_revid, revid_list
128.1.46 by Michael Hudson
delete-trailing-whitespace
467
42 by Robey Pointer
add text substring indexer
468
        # potentially limit the search
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
469
        if file_id is not None:
470
            revid_list = self.get_file_view(start_revid, file_id)
42 by Robey Pointer
add text substring indexer
471
        else:
472
            revid_list = None
473
159.1.5 by Michael Hudson
restore fix_year, get rid of try:except:
474
        revid_list = self.get_search_revid_list(query, revid_list)
475
        if revid_list and len(revid_list) > 0:
476
            if revid not in revid_list:
477
                revid = revid_list[0]
478
            return revid, start_revid, revid_list
479
        else:
42 by Robey Pointer
add text substring indexer
480
            return None, None, []
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
481
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
482
    @with_branch_lock
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
483
    def get_inventory(self, revid):
484
        return self._branch.repository.get_revision_inventory(revid)
1 by Robey Pointer
initial checkin
485
38 by Robey Pointer
another pile of semi-related changes:
486
    @with_branch_lock
487
    def get_path(self, revid, file_id):
488
        if (file_id is None) or (file_id == ''):
489
            return ''
490
        path = self._branch.repository.get_revision_inventory(revid).id2path(file_id)
491
        if (len(path) > 0) and not path.startswith('/'):
492
            path = '/' + path
493
        return path
128.1.46 by Michael Hudson
delete-trailing-whitespace
494
125 by Robey Pointer
bug 98826: allow 'annotate' to take a path on the URL line instead of a
495
    @with_branch_lock
496
    def get_file_id(self, revid, path):
497
        if (len(path) > 0) and not path.startswith('/'):
498
            path = '/' + path
499
        return self._branch.repository.get_revision_inventory(revid).path2id(path)
128.1.46 by Michael Hudson
delete-trailing-whitespace
500
1 by Robey Pointer
initial checkin
501
    def get_merge_point_list(self, revid):
502
        """
503
        Return the list of revids that have merged this node.
504
        """
128.1.21 by Michael Hudson
kill more dead code (this might help startup time a bit, even)
505
        if '.' not in self.get_revno(revid):
1 by Robey Pointer
initial checkin
506
            return []
128.1.46 by Michael Hudson
delete-trailing-whitespace
507
1 by Robey Pointer
initial checkin
508
        merge_point = []
509
        while True:
128.1.23 by Michael Hudson
even more simplifications
510
            children = self._where_merged.get(revid, [])
1 by Robey Pointer
initial checkin
511
            nexts = []
512
            for child in children:
513
                child_parents = self._revision_graph[child]
514
                if child_parents[0] == revid:
515
                    nexts.append(child)
516
                else:
517
                    merge_point.append(child)
518
519
            if len(nexts) == 0:
520
                # only merge
521
                return merge_point
522
523
            while len(nexts) > 1:
524
                # branch
525
                next = nexts.pop()
526
                merge_point_next = self.get_merge_point_list(next)
527
                merge_point.extend(merge_point_next)
528
529
            revid = nexts[0]
128.1.46 by Michael Hudson
delete-trailing-whitespace
530
1 by Robey Pointer
initial checkin
531
    def simplify_merge_point_list(self, revids):
532
        """if a revision is already merged, don't show further merge points"""
533
        d = {}
534
        for revid in revids:
535
            revno = self.get_revno(revid)
536
            revnol = revno.split(".")
537
            revnos = ".".join(revnol[:-2])
538
            revnolast = int(revnol[-1])
539
            if d.has_key(revnos):
540
                m = d[revnos][0]
541
                if revnolast < m:
542
                    d[revnos] = ( revnolast, revid )
543
            else:
544
                d[revnos] = ( revnolast, revid )
545
546
        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
547
548
    def get_branch_nicks(self, changes):
549
        """
550
        given a list of changes from L{get_changes}, fill in the branch nicks
551
        on all parents and merge points.
552
        """
553
        fetch_set = set()
554
        for change in changes:
555
            for p in change.parents:
556
                fetch_set.add(p.revid)
557
            for p in change.merge_points:
558
                fetch_set.add(p.revid)
559
        p_changes = self.get_changes(list(fetch_set))
560
        p_change_dict = dict([(c.revid, c) for c in p_changes])
561
        for change in changes:
112 by Robey Pointer
branches imported from arch/tla can have "merges" from revisions that don't
562
            # arch-converted branches may not have merged branch info :(
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
563
            for p in change.parents:
112 by Robey Pointer
branches imported from arch/tla can have "merges" from revisions that don't
564
                if p.revid in p_change_dict:
565
                    p.branch_nick = p_change_dict[p.revid].branch_nick
566
                else:
567
                    p.branch_nick = '(missing)'
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
568
            for p in change.merge_points:
112 by Robey Pointer
branches imported from arch/tla can have "merges" from revisions that don't
569
                if p.revid in p_change_dict:
570
                    p.branch_nick = p_change_dict[p.revid].branch_nick
571
                else:
572
                    p.branch_nick = '(missing)'
128.1.46 by Michael Hudson
delete-trailing-whitespace
573
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
574
    @with_branch_lock
128.1.47 by Michael Hudson
kill the get_diffs argument to get_changes entirely.
575
    def get_changes(self, revid_list):
144.1.7 by Michael Hudson
add just one docstring
576
        """Return a list of changes objects for the given revids.
577
578
        Revisions not present and NULL_REVISION will be ignored.
579
        """
128.6.76 by Martin Albisetti
* Remove fulltext cache
580
        changes = self.get_changes_uncached(revid_list)
128.1.15 by Michael Hudson
fix bug #92435, but by hacking not by understanding
581
        if len(changes) == 0:
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
582
            return changes
128.1.46 by Michael Hudson
delete-trailing-whitespace
583
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
584
        # some data needs to be recalculated each time, because it may
585
        # 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
586
        for change in changes:
587
            merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
588
            change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
128.2.20 by Robey Pointer
unfiled bug that i just noticed today:
589
            if len(change.parents) > 0:
128.6.76 by Martin Albisetti
* Remove fulltext cache
590
                change.parents = [util.Container(revid=r, 
591
                    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
592
            change.revno = self.get_revno(change.revid)
113.1.3 by Kent Gibson
Rework changes page to provide single line summary per change.
593
594
        parity = 0
595
        for change in changes:
596
            change.parity = parity
597
            parity ^= 1
128.1.46 by Michael Hudson
delete-trailing-whitespace
598
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
599
        return changes
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
600
128.2.18 by Robey Pointer
rename get_diff to get_change_relative_to (since it returns the entire
601
    @with_branch_lock
602
    @with_bzrlib_read_lock
603
    def get_changes_uncached(self, revid_list):
128.8.1 by Martin Albisetti
Merge in changes with trunk
604
        # FIXME: deprecated method in getting a null revision
144.1.3 by Michael Hudson
fix two more tests
605
        revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
606
                            revid_list)
128.6.76 by Martin Albisetti
* Remove fulltext cache
607
        parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
148.1.3 by Michael Hudson
use a list comprehension instead
608
        # We need to return the answer in the same order as the input,
609
        # less any ghosts.
610
        present_revids = [revid for revid in revid_list
611
                          if revid in parent_map]
128.6.76 by Martin Albisetti
* Remove fulltext cache
612
        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
613
144.1.2 by Michael Hudson
use less dumb method of filtering revisions
614
        return [self._change_from_revision(rev) for rev in rev_list]
128.1.46 by Michael Hudson
delete-trailing-whitespace
615
85 by Robey Pointer
try john's idea of implementing _get_deltas_for_revisions_with_trees().
616
    def _get_deltas_for_revisions_with_trees(self, revisions):
128.2.18 by Robey Pointer
rename get_diff to get_change_relative_to (since it returns the entire
617
        """Produce a list of revision deltas.
128.6.16 by Michael Hudson
d-t-w in history.py
618
85 by Robey Pointer
try john's idea of implementing _get_deltas_for_revisions_with_trees().
619
        Note that the input is a sequence of REVISIONS, not revision_ids.
620
        Trees will be held in memory until the generator exits.
621
        Each delta is relative to the revision's lefthand predecessor.
128.2.18 by Robey Pointer
rename get_diff to get_change_relative_to (since it returns the entire
622
        (This is copied from bzrlib.)
85 by Robey Pointer
try john's idea of implementing _get_deltas_for_revisions_with_trees().
623
        """
624
        required_trees = set()
625
        for revision in revisions:
128.2.30 by Robey Pointer
merge from less-file-change-access. i resolved a lot of conflicts but i'm
626
            required_trees.add(revision.revid)
627
            required_trees.update([p.revid for p in revision.parents[:1]])
128.1.46 by Michael Hudson
delete-trailing-whitespace
628
        trees = dict((t.get_revision_id(), t) for
85 by Robey Pointer
try john's idea of implementing _get_deltas_for_revisions_with_trees().
629
                     t in self._branch.repository.revision_trees(required_trees))
630
        ret = []
631
        self._branch.repository.lock_read()
632
        try:
140.2.2 by Michael Hudson
oops
633
            for revision in revisions:
634
                if not revision.parents:
128.1.60 by Michael Hudson
respond to review comments
635
                    old_tree = self._branch.repository.revision_tree(
636
                        bzrlib.revision.NULL_REVISION)
85 by Robey Pointer
try john's idea of implementing _get_deltas_for_revisions_with_trees().
637
                else:
128.2.30 by Robey Pointer
merge from less-file-change-access. i resolved a lot of conflicts but i'm
638
                    old_tree = trees[revision.parents[0].revid]
639
                tree = trees[revision.revid]
128.1.45 by Michael Hudson
clean up some signatures
640
                ret.append(tree.changes_from(old_tree))
85 by Robey Pointer
try john's idea of implementing _get_deltas_for_revisions_with_trees().
641
            return ret
642
        finally:
643
            self._branch.repository.unlock()
128.1.46 by Michael Hudson
delete-trailing-whitespace
644
128.2.18 by Robey Pointer
rename get_diff to get_change_relative_to (since it returns the entire
645
    def _change_from_revision(self, revision):
646
        """
647
        Given a bzrlib Revision, return a processed "change" for use in
648
        templates.
649
        """
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
650
        commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
128.1.46 by Michael Hudson
delete-trailing-whitespace
651
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
652
        parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
653
654
        message, short_message = clean_message(revision.message)
655
656
        entry = {
657
            'revid': revision.revision_id,
658
            'date': commit_time,
170 by Martin Albisetti
* Show author instead of committer. Bug #149443
659
            'author': revision.get_apparent_author(),
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
660
            'branch_nick': revision.properties.get('branch-nick', None),
661
            'short_comment': short_message,
662
            'comment': revision.message,
663
            'comment_clean': [util.html_clean(s) for s in message],
128.2.20 by Robey Pointer
unfiled bug that i just noticed today:
664
            'parents': revision.parent_ids,
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
665
        }
666
        return util.Container(entry)
667
128.1.52 by Michael Hudson
random tidying towards being able to add caching of changed file lists
668
    def get_file_changes_uncached(self, entries):
669
        delta_list = self._get_deltas_for_revisions_with_trees(entries)
670
671
        return [self.parse_delta(delta) for delta in delta_list]
672
128.1.48 by Michael Hudson
don't compute the changed files by default, as it's expensive and rarely used.
673
    @with_branch_lock
128.1.52 by Michael Hudson
random tidying towards being able to add caching of changed file lists
674
    def get_file_changes(self, entries):
128.1.55 by Michael Hudson
plumbing for a file change cache
675
        if self._file_change_cache is None:
676
            return self.get_file_changes_uncached(entries)
677
        else:
678
            return self._file_change_cache.get_file_changes(entries)
128.1.52 by Michael Hudson
random tidying towards being able to add caching of changed file lists
679
128.1.48 by Michael Hudson
don't compute the changed files by default, as it's expensive and rarely used.
680
    def add_changes(self, entries):
128.1.52 by Michael Hudson
random tidying towards being able to add caching of changed file lists
681
        changes_list = self.get_file_changes(entries)
128.1.48 by Michael Hudson
don't compute the changed files by default, as it's expensive and rarely used.
682
128.1.52 by Michael Hudson
random tidying towards being able to add caching of changed file lists
683
        for entry, changes in zip(entries, changes_list):
684
            entry.changes = changes
35 by Robey Pointer
makeover of revision fetching, based on some hints on the bazaar mailing
685
128.1.51 by Michael Hudson
some refactoring to avoid duplication of work
686
    @with_branch_lock
128.1.47 by Michael Hudson
kill the get_diffs argument to get_changes entirely.
687
    def get_change_with_diff(self, revid, compare_revid=None):
128.2.30 by Robey Pointer
merge from less-file-change-access. i resolved a lot of conflicts but i'm
688
        change = self.get_changes([revid])[0]
128.1.51 by Michael Hudson
some refactoring to avoid duplication of work
689
128.1.47 by Michael Hudson
kill the get_diffs argument to get_changes entirely.
690
        if compare_revid is None:
128.2.30 by Robey Pointer
merge from less-file-change-access. i resolved a lot of conflicts but i'm
691
            if change.parents:
692
                compare_revid = change.parents[0].revid
128.1.47 by Michael Hudson
kill the get_diffs argument to get_changes entirely.
693
            else:
694
                compare_revid = 'null:'
128.1.51 by Michael Hudson
some refactoring to avoid duplication of work
695
696
        rev_tree1 = self._branch.repository.revision_tree(compare_revid)
697
        rev_tree2 = self._branch.repository.revision_tree(revid)
698
        delta = rev_tree2.changes_from(rev_tree1)
699
128.2.30 by Robey Pointer
merge from less-file-change-access. i resolved a lot of conflicts but i'm
700
        change.changes = self.parse_delta(delta)
701
        change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
702
128.2.18 by Robey Pointer
rename get_diff to get_change_relative_to (since it returns the entire
703
        return change
128.1.46 by Michael Hudson
delete-trailing-whitespace
704
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
705
    @with_branch_lock
38 by Robey Pointer
another pile of semi-related changes:
706
    def get_file(self, file_id, revid):
106 by Robey Pointer
oops, fix up the download logging for real.
707
        "returns (path, filename, data)"
708
        inv = self.get_inventory(revid)
709
        inv_entry = inv[file_id]
37 by Robey Pointer
don't bother to rebuild the cache when it's full
710
        rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
106 by Robey Pointer
oops, fix up the download logging for real.
711
        path = inv.id2path(file_id)
712
        if not path.startswith('/'):
713
            path = '/' + path
714
        return path, inv_entry.name, rev_tree.get_file_text(file_id)
128.1.46 by Michael Hudson
delete-trailing-whitespace
715
128.1.51 by Michael Hudson
some refactoring to avoid duplication of work
716
    def _parse_diffs(self, old_tree, new_tree, delta):
3 by Robey Pointer
possibly i'm going a little crazy here, but dramatically improve diff output by parsing it into a nested structure and letting the template format it
717
        """
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
718
        Return a list of processed diffs, in the format::
128.1.46 by Michael Hudson
delete-trailing-whitespace
719
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
720
            list(
3 by Robey Pointer
possibly i'm going a little crazy here, but dramatically improve diff output by parsing it into a nested structure and letting the template format it
721
                filename: str,
38 by Robey Pointer
another pile of semi-related changes:
722
                file_id: str,
3 by Robey Pointer
possibly i'm going a little crazy here, but dramatically improve diff output by parsing it into a nested structure and letting the template format it
723
                chunks: list(
724
                    diff: list(
725
                        old_lineno: int,
726
                        new_lineno: int,
727
                        type: str('context', 'delete', or 'insert'),
728
                        line: str,
729
                    ),
730
                ),
731
            )
732
        """
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
733
        process = []
734
        out = []
128.1.46 by Michael Hudson
delete-trailing-whitespace
735
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
736
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
737
            if text_modified:
738
                process.append((old_path, new_path, fid, kind))
739
        for path, fid, kind, text_modified, meta_modified in delta.modified:
740
            process.append((path, path, fid, kind))
128.1.46 by Michael Hudson
delete-trailing-whitespace
741
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
742
        for old_path, new_path, fid, kind in process:
37 by Robey Pointer
don't bother to rebuild the cache when it's full
743
            old_lines = old_tree.get_file_lines(fid)
744
            new_lines = new_tree.get_file_lines(fid)
3 by Robey Pointer
possibly i'm going a little crazy here, but dramatically improve diff output by parsing it into a nested structure and letting the template format it
745
            buffer = StringIO()
128.1.37 by Michael Hudson
test and paper over the fact that changing just the execute bit of a file in a
746
            if old_lines != new_lines:
747
                try:
748
                    bzrlib.diff.internal_diff(old_path, old_lines,
749
                                              new_path, new_lines, buffer)
750
                except bzrlib.errors.BinaryFile:
751
                    diff = ''
752
                else:
753
                    diff = buffer.getvalue()
754
            else:
114.2.2 by James Henstridge
Use an empty string for binary files
755
                diff = ''
155.1.2 by Michael Hudson
show diffs in plain text when above a currently hard-coded limit
756
            out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
128.1.46 by Michael Hudson
delete-trailing-whitespace
757
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
758
        return out
3 by Robey Pointer
possibly i'm going a little crazy here, but dramatically improve diff output by parsing it into a nested structure and letting the template format it
759
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
760
    def _process_diff(self, diff):
761
        # doesn't really need to be a method; could be static.
762
        chunks = []
763
        chunk = None
764
        for line in diff.splitlines():
765
            if len(line) == 0:
766
                continue
767
            if line.startswith('+++ ') or line.startswith('--- '):
768
                continue
769
            if line.startswith('@@ '):
770
                # new chunk
771
                if chunk is not None:
772
                    chunks.append(chunk)
773
                chunk = util.Container()
774
                chunk.diff = []
775
                lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
776
                old_lineno = lines[0]
777
                new_lineno = lines[1]
778
            elif line.startswith(' '):
779
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
780
                                                 type='context', line=util.fixed_width(line[1:])))
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
781
                old_lineno += 1
782
                new_lineno += 1
783
            elif line.startswith('+'):
784
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
785
                                                 type='insert', line=util.fixed_width(line[1:])))
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
786
                new_lineno += 1
787
            elif line.startswith('-'):
788
                chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
789
                                                 type='delete', line=util.fixed_width(line[1:])))
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
790
                old_lineno += 1
791
            else:
792
                chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
793
                                                 type='unknown', line=util.fixed_width(repr(line))))
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
794
        if chunk is not None:
795
            chunks.append(chunk)
796
        return chunks
128.1.46 by Michael Hudson
delete-trailing-whitespace
797
128.1.47 by Michael Hudson
kill the get_diffs argument to get_changes entirely.
798
    def parse_delta(self, delta):
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
799
        """
800
        Return a nested data structure containing the changes in a delta::
128.1.46 by Michael Hudson
delete-trailing-whitespace
801
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
802
            added: list((filename, file_id)),
803
            renamed: list((old_filename, new_filename, file_id)),
804
            deleted: list((filename, file_id)),
805
            modified: list(
806
                filename: str,
807
                file_id: str,
808
            )
809
        """
810
        added = []
811
        modified = []
812
        renamed = []
813
        removed = []
128.1.46 by Michael Hudson
delete-trailing-whitespace
814
2 by Robey Pointer
add revision page, put some of the config in the config file
815
        for path, fid, kind in delta.added:
38 by Robey Pointer
another pile of semi-related changes:
816
            added.append((rich_filename(path, kind), fid))
128.1.46 by Michael Hudson
delete-trailing-whitespace
817
2 by Robey Pointer
add revision page, put some of the config in the config file
818
        for path, fid, kind, text_modified, meta_modified in delta.modified:
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
819
            modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
128.1.46 by Michael Hudson
delete-trailing-whitespace
820
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
821
        for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
822
            renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
2 by Robey Pointer
add revision page, put some of the config in the config file
823
            if meta_modified or text_modified:
128.2.7 by Robey Pointer
the diff cache isn't adding very much, and can grow very large. let's just
824
                modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
128.1.46 by Michael Hudson
delete-trailing-whitespace
825
2 by Robey Pointer
add revision page, put some of the config in the config file
826
        for path, fid, kind in delta.removed:
38 by Robey Pointer
another pile of semi-related changes:
827
            removed.append((rich_filename(path, kind), fid))
128.1.46 by Michael Hudson
delete-trailing-whitespace
828
3 by Robey Pointer
possibly i'm going a little crazy here, but dramatically improve diff output by parsing it into a nested structure and letting the template format it
829
        return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
830
66 by Robey Pointer
do side-by-side diff on the revision page, making it the default.
831
    @staticmethod
81 by Robey Pointer
overhauled the collapse/expand buttons: pulled out into a separate js file,
832
    def add_side_by_side(changes):
66 by Robey Pointer
do side-by-side diff on the revision page, making it the default.
833
        # FIXME: this is a rotten API.
834
        for change in changes:
835
            for m in change.changes.modified:
81 by Robey Pointer
overhauled the collapse/expand buttons: pulled out into a separate js file,
836
                m.sbs_chunks = _make_side_by_side(m.chunks)
128.1.46 by Michael Hudson
delete-trailing-whitespace
837
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
838
    @with_branch_lock
128.1.38 by Michael Hudson
rewrite get_filelist to not loop over all the entries in the inventory
839
    def get_filelist(self, inv, file_id, sort_type=None):
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
840
        """
841
        return the list of all files (and their attributes) within a given
842
        path subtree.
843
        """
128.1.38 by Michael Hudson
rewrite get_filelist to not loop over all the entries in the inventory
844
845
        dir_ie = inv[file_id]
846
        path = inv.id2path(file_id)
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
847
        file_list = []
128.1.38 by Michael Hudson
rewrite get_filelist to not loop over all the entries in the inventory
848
128.1.49 by Michael Hudson
wow, get_revisions takes time more or less independent of the number of
849
        revid_set = set()
850
851
        for filename, entry in dir_ie.children.iteritems():
852
            revid_set.add(entry.revision)
853
854
        change_dict = {}
855
        for change in self.get_changes(list(revid_set)):
856
            change_dict[change.revid] = change
857
128.1.38 by Michael Hudson
rewrite get_filelist to not loop over all the entries in the inventory
858
        for filename, entry in dir_ie.children.iteritems():
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
859
            pathname = filename
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
860
            if entry.kind == 'directory':
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
861
                pathname += '/'
128.1.9 by Michael Hudson
hack around in get_filelist so it doesn't call get_changes at all.
862
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
863
            revid = entry.revision
128.1.38 by Michael Hudson
rewrite get_filelist to not loop over all the entries in the inventory
864
128.1.39 by Michael Hudson
make history.get_file_view have a clearer interface
865
            file = util.Container(
866
                filename=filename, executable=entry.executable, kind=entry.kind,
867
                pathname=pathname, file_id=entry.file_id, size=entry.text_size,
128.1.49 by Michael Hudson
wow, get_revisions takes time more or less independent of the number of
868
                revid=revid, change=change_dict[revid])
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
869
            file_list.append(file)
128.1.38 by Michael Hudson
rewrite get_filelist to not loop over all the entries in the inventory
870
871
        if sort_type == 'filename' or sort_type is None:
170.1.2 by Martin Albisetti
* Only show directories first
872
            file_list.sort(key=lambda x: x.filename.lower()) # case-insensitive
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
873
        elif sort_type == 'size':
874
            file_list.sort(key=lambda x: x.size)
875
        elif sort_type == 'date':
876
            file_list.sort(key=lambda x: x.change.date)
170.1.1 by Martin Albisetti
* Sort directories before files
877
        
878
        # Always sort by kind to get directories first
170.1.2 by Martin Albisetti
* Only show directories first
879
        file_list.sort(key=lambda x: x.kind != 'directory')
128.1.38 by Michael Hudson
rewrite get_filelist to not loop over all the entries in the inventory
880
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
881
        parity = 0
882
        for file in file_list:
883
            file.parity = parity
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
884
            parity ^= 1
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
885
886
        return file_list
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
887
38 by Robey Pointer
another pile of semi-related changes:
888
127 by Robey Pointer
bug 113313: remove 0x0C (page break) from the list of characters that
889
    _BADCHARS_RE = re.compile(ur'[\x00-\x08\x0b\x0e-\x1f]')
38 by Robey Pointer
another pile of semi-related changes:
890
21 by Robey Pointer
fix a thread-unsafety bug in bzrlib's progress bar system that was vexing
891
    @with_branch_lock
15 by Robey Pointer
add an annotate page, and rename inventory -> files
892
    def annotate_file(self, file_id, revid):
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
893
        z = time.time()
15 by Robey Pointer
add an annotate page, and rename inventory -> files
894
        lineno = 1
895
        parity = 0
128.1.46 by Michael Hudson
delete-trailing-whitespace
896
15 by Robey Pointer
add an annotate page, and rename inventory -> files
897
        file_revid = self.get_inventory(revid)[file_id].revision
898
        oldvalues = None
156.1.4 by Martin Albisetti
* Change deprecated method used to generate annotate
899
        tree = self._branch.repository.revision_tree(file_revid)
156.1.5 by Martin Albisetti
Remove obsolete comment
900
        revid_set = set()
128.1.46 by Michael Hudson
delete-trailing-whitespace
901
156.1.4 by Martin Albisetti
* Change deprecated method used to generate annotate
902
        for line_revid, text in tree.annotate_iter(file_id):
37 by Robey Pointer
don't bother to rebuild the cache when it's full
903
            revid_set.add(line_revid)
38 by Robey Pointer
another pile of semi-related changes:
904
            if self._BADCHARS_RE.match(text):
905
                # bail out; this isn't displayable text
906
                yield util.Container(parity=0, lineno=1, status='same',
128.2.9 by Robey Pointer
merge mwhudson's branch. amusingly, we came up with nearly identical fixes
907
                                     text='(This is a binary file.)',
38 by Robey Pointer
another pile of semi-related changes:
908
                                     change=util.Container())
909
                return
156.1.4 by Martin Albisetti
* Change deprecated method used to generate annotate
910
        change_cache = dict([(c.revid, c) \
911
                for c in self.get_changes(list(revid_set))])
128.1.46 by Michael Hudson
delete-trailing-whitespace
912
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
913
        last_line_revid = None
156.1.4 by Martin Albisetti
* Change deprecated method used to generate annotate
914
        for line_revid, text in tree.annotate_iter(file_id):
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
915
            if line_revid == last_line_revid:
916
                # remember which lines have a new revno and which don't
15 by Robey Pointer
add an annotate page, and rename inventory -> files
917
                status = 'same'
918
            else:
919
                status = 'changed'
920
                parity ^= 1
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
921
                last_line_revid = line_revid
37 by Robey Pointer
don't bother to rebuild the cache when it's full
922
                change = change_cache[line_revid]
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
923
                trunc_revno = change.revno
924
                if len(trunc_revno) > 10:
925
                    trunc_revno = trunc_revno[:9] + '...'
128.1.25 by Michael Hudson
improve the rendering performance of annotate pages by a factor of 4 or so by
926
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
927
            yield util.Container(parity=parity, lineno=lineno, status=status,
128.2.9 by Robey Pointer
merge mwhudson's branch. amusingly, we came up with nearly identical fixes
928
                                 change=change, text=util.fixed_width(text))
15 by Robey Pointer
add an annotate page, and rename inventory -> files
929
            lineno += 1
128.1.46 by Michael Hudson
delete-trailing-whitespace
930
47 by Robey Pointer
slowly moving the branch-specific stuff into a common structure...
931
        self.log.debug('annotate: %r secs' % (time.time() - z,))