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