/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/log.py

  • Committer: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Canonical Ltd
2
 
 
 
1
# Copyright (C) 2005-2011 Canonical Ltd
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
 
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
16
 
19
17
"""Code to show logs of changes.
20
18
 
26
24
* in "verbose" mode with a description of what changed from one
27
25
  version to the next
28
26
 
29
 
* with file-ids and revision-ids shown
 
27
* with files and revision-ids shown
30
28
 
31
29
Logs are actually written out through an abstract LogFormatter
32
30
interface, which allows for different preferred formats.  Plugins can
42
40
 
43
41
In verbose mode we show a summary of what changed in each particular
44
42
revision.  Note that this is the delta for changes in that revision
45
 
relative to its mainline parent, not the delta relative to the last
 
43
relative to its left-most parent, not the delta relative to the last
46
44
logged revision.  So for example if you ask for a verbose log of
47
45
changes touching hello.c you will get a list of those revisions also
48
46
listing other things that were changed in the same revision, but not
49
47
all the changes since the previous revision that touched hello.c.
50
48
"""
51
49
 
52
 
 
53
 
from bzrlib.tree import EmptyTree
54
 
from bzrlib.delta import compare_trees
55
 
from bzrlib.trace import mutter
56
 
 
57
 
 
58
 
def find_touching_revisions(branch, file_id):
59
 
    """Yield a description of revisions which affect the file_id.
 
50
import codecs
 
51
from io import BytesIO
 
52
import itertools
 
53
import re
 
54
import sys
 
55
from warnings import (
 
56
    warn,
 
57
    )
 
58
 
 
59
from .lazy_import import lazy_import
 
60
lazy_import(globals(), """
 
61
 
 
62
from breezy import (
 
63
    config,
 
64
    controldir,
 
65
    diff,
 
66
    foreign,
 
67
    lazy_regex,
 
68
    revision as _mod_revision,
 
69
    )
 
70
from breezy.i18n import gettext, ngettext
 
71
""")
 
72
 
 
73
from . import (
 
74
    errors,
 
75
    registry,
 
76
    revisionspec,
 
77
    trace,
 
78
    )
 
79
from .osutils import (
 
80
    format_date,
 
81
    format_date_with_offset_in_original_timezone,
 
82
    get_diff_header_encoding,
 
83
    get_terminal_encoding,
 
84
    is_inside,
 
85
    terminal_width,
 
86
    )
 
87
from .tree import (
 
88
    find_previous_path,
 
89
    InterTree,
 
90
    )
 
91
 
 
92
 
 
93
def find_touching_revisions(repository, last_revision, last_tree, last_path):
 
94
    """Yield a description of revisions which affect the file.
60
95
 
61
96
    Each returned element is (revno, revision_id, description)
62
97
 
66
101
    TODO: Perhaps some way to limit this to only particular revisions,
67
102
    or to traverse a non-mainline set of revisions?
68
103
    """
69
 
    last_ie = None
70
 
    last_path = None
71
 
    revno = 1
72
 
    for revision_id in branch.revision_history():
73
 
        this_inv = branch.get_revision_inventory(revision_id)
74
 
        if file_id in this_inv:
75
 
            this_ie = this_inv[file_id]
76
 
            this_path = this_inv.id2path(file_id)
77
 
        else:
78
 
            this_ie = this_path = None
 
104
    last_verifier = last_tree.get_file_verifier(last_path)
 
105
    graph = repository.get_graph()
 
106
    history = list(graph.iter_lefthand_ancestry(last_revision, []))
 
107
    revno = len(history)
 
108
    for revision_id in history:
 
109
        this_tree = repository.revision_tree(revision_id)
 
110
        this_intertree = InterTree.get(this_tree, last_tree)
 
111
        this_path = this_intertree.find_source_path(last_path)
79
112
 
80
113
        # now we know how it was last time, and how it is in this revision.
81
114
        # are those two states effectively the same or not?
82
 
 
83
 
        if not this_ie and not last_ie:
84
 
            # not present in either
85
 
            pass
86
 
        elif this_ie and not last_ie:
87
 
            yield revno, revision_id, "added " + this_path
88
 
        elif not this_ie and last_ie:
89
 
            # deleted here
90
 
            yield revno, revision_id, "deleted " + last_path
 
115
        if this_path is not None and last_path is None:
 
116
            yield revno, revision_id, "deleted " + this_path
 
117
            this_verifier = this_tree.get_file_verifier(this_path)
 
118
        elif this_path is None and last_path is not None:
 
119
            yield revno, revision_id, "added " + last_path
91
120
        elif this_path != last_path:
92
 
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
93
 
        elif (this_ie.text_size != last_ie.text_size
94
 
              or this_ie.text_sha1 != last_ie.text_sha1):
95
 
            yield revno, revision_id, "modified " + this_path
 
121
            yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
 
122
            this_verifier = this_tree.get_file_verifier(this_path)
 
123
        else:
 
124
            this_verifier = this_tree.get_file_verifier(this_path)
 
125
            if (this_verifier != last_verifier):
 
126
                yield revno, revision_id, "modified " + this_path
96
127
 
97
 
        last_ie = this_ie
 
128
        last_verifier = this_verifier
98
129
        last_path = this_path
99
 
        revno += 1
100
 
 
101
 
 
102
 
 
103
 
def _enumerate_history(branch):
104
 
    rh = []
105
 
    revno = 1
106
 
    for rev_id in branch.revision_history():
107
 
        rh.append((revno, rev_id))
108
 
        revno += 1
109
 
    return rh
 
130
        last_tree = this_tree
 
131
        if last_path is None:
 
132
            return
 
133
        revno -= 1
110
134
 
111
135
 
112
136
def show_log(branch,
113
137
             lf,
114
 
             specific_fileid=None,
115
138
             verbose=False,
116
139
             direction='reverse',
117
140
             start_revision=None,
118
141
             end_revision=None,
119
 
             search=None):
 
142
             limit=None,
 
143
             show_diff=False,
 
144
             match=None):
120
145
    """Write out human-readable log of commits to this branch.
121
146
 
122
 
    lf
123
 
        LogFormatter object to show the output.
124
 
 
125
 
    specific_fileid
126
 
        If true, list only the commits affecting the specified
127
 
        file, rather than all commits.
128
 
 
129
 
    verbose
130
 
        If true show added/changed/deleted/renamed files.
131
 
 
132
 
    direction
133
 
        'reverse' (default) is latest to earliest;
134
 
        'forward' is earliest to latest.
135
 
 
136
 
    start_revision
137
 
        If not None, only show revisions >= start_revision
138
 
 
139
 
    end_revision
140
 
        If not None, only show revisions <= end_revision
141
 
    """
142
 
    from bzrlib.osutils import format_date
143
 
    from bzrlib.errors import BzrCheckError
144
 
    from bzrlib.textui import show_status
145
 
    
146
 
    from warnings import warn
147
 
 
148
 
    if not isinstance(lf, LogFormatter):
149
 
        warn("not a LogFormatter instance: %r" % lf)
150
 
 
151
 
    if specific_fileid:
152
 
        mutter('get log for file_id %r' % specific_fileid)
153
 
 
154
 
    if search is not None:
155
 
        import re
156
 
        searchRE = re.compile(search, re.IGNORECASE)
157
 
    else:
158
 
        searchRE = None
159
 
 
160
 
    which_revs = _enumerate_history(branch)
161
 
    
 
147
    This function is being retained for backwards compatibility but
 
148
    should not be extended with new parameters. Use the new Logger class
 
149
    instead, eg. Logger(branch, rqst).show(lf), adding parameters to the
 
150
    make_log_request_dict function.
 
151
 
 
152
    :param lf: The LogFormatter object showing the output.
 
153
 
 
154
    :param verbose: If True show added/changed/deleted/renamed files.
 
155
 
 
156
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
 
157
        earliest to latest.
 
158
 
 
159
    :param start_revision: If not None, only show revisions >= start_revision
 
160
 
 
161
    :param end_revision: If not None, only show revisions <= end_revision
 
162
 
 
163
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
 
164
        if None or 0.
 
165
 
 
166
    :param show_diff: If True, output a diff after each revision.
 
167
 
 
168
    :param match: Dictionary of search lists to use when matching revision
 
169
      properties.
 
170
    """
 
171
    if verbose:
 
172
        delta_type = 'full'
 
173
    else:
 
174
        delta_type = None
 
175
    if show_diff:
 
176
        diff_type = 'full'
 
177
    else:
 
178
        diff_type = None
 
179
 
 
180
    if isinstance(start_revision, int):
 
181
        try:
 
182
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
 
183
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
184
            raise errors.InvalidRevisionNumber(start_revision)
 
185
 
 
186
    if isinstance(end_revision, int):
 
187
        try:
 
188
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
 
189
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
190
            raise errors.InvalidRevisionNumber(end_revision)
 
191
 
 
192
    if end_revision is not None and end_revision.revno == 0:
 
193
        raise errors.InvalidRevisionNumber(end_revision.revno)
 
194
 
 
195
    # Build the request and execute it
 
196
    rqst = make_log_request_dict(
 
197
        direction=direction,
 
198
        start_revision=start_revision, end_revision=end_revision,
 
199
        limit=limit, delta_type=delta_type, diff_type=diff_type)
 
200
    Logger(branch, rqst).show(lf)
 
201
 
 
202
 
 
203
# Note: This needs to be kept in sync with the defaults in
 
204
# make_log_request_dict() below
 
205
_DEFAULT_REQUEST_PARAMS = {
 
206
    'direction': 'reverse',
 
207
    'levels': None,
 
208
    'generate_tags': True,
 
209
    'exclude_common_ancestry': False,
 
210
    '_match_using_deltas': True,
 
211
    }
 
212
 
 
213
 
 
214
def make_log_request_dict(direction='reverse', specific_files=None,
 
215
                          start_revision=None, end_revision=None, limit=None,
 
216
                          message_search=None, levels=None, generate_tags=True,
 
217
                          delta_type=None,
 
218
                          diff_type=None, _match_using_deltas=True,
 
219
                          exclude_common_ancestry=False, match=None,
 
220
                          signature=False, omit_merges=False,
 
221
                          ):
 
222
    """Convenience function for making a logging request dictionary.
 
223
 
 
224
    Using this function may make code slightly safer by ensuring
 
225
    parameters have the correct names. It also provides a reference
 
226
    point for documenting the supported parameters.
 
227
 
 
228
    :param direction: 'reverse' (default) is latest to earliest;
 
229
      'forward' is earliest to latest.
 
230
 
 
231
    :param specific_files: If not None, only include revisions
 
232
      affecting the specified files, rather than all revisions.
 
233
 
 
234
    :param start_revision: If not None, only generate
 
235
      revisions >= start_revision
 
236
 
 
237
    :param end_revision: If not None, only generate
 
238
      revisions <= end_revision
 
239
 
 
240
    :param limit: If set, generate only 'limit' revisions, all revisions
 
241
      are shown if None or 0.
 
242
 
 
243
    :param message_search: If not None, only include revisions with
 
244
      matching commit messages
 
245
 
 
246
    :param levels: the number of levels of revisions to
 
247
      generate; 1 for just the mainline; 0 for all levels, or None for
 
248
      a sensible default.
 
249
 
 
250
    :param generate_tags: If True, include tags for matched revisions.
 
251
`
 
252
    :param delta_type: Either 'full', 'partial' or None.
 
253
      'full' means generate the complete delta - adds/deletes/modifies/etc;
 
254
      'partial' means filter the delta using specific_files;
 
255
      None means do not generate any delta.
 
256
 
 
257
    :param diff_type: Either 'full', 'partial' or None.
 
258
      'full' means generate the complete diff - adds/deletes/modifies/etc;
 
259
      'partial' means filter the diff using specific_files;
 
260
      None means do not generate any diff.
 
261
 
 
262
    :param _match_using_deltas: a private parameter controlling the
 
263
      algorithm used for matching specific_files. This parameter
 
264
      may be removed in the future so breezy client code should NOT
 
265
      use it.
 
266
 
 
267
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
 
268
      range operator or as a graph difference.
 
269
 
 
270
    :param signature: show digital signature information
 
271
 
 
272
    :param match: Dictionary of list of search strings to use when filtering
 
273
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
274
      the empty string to match any of the preceding properties.
 
275
 
 
276
    :param omit_merges: If True, commits with more than one parent are
 
277
      omitted.
 
278
 
 
279
    """
 
280
    # Take care of old style message_search parameter
 
281
    if message_search:
 
282
        if match:
 
283
            if 'message' in match:
 
284
                match['message'].append(message_search)
 
285
            else:
 
286
                match['message'] = [message_search]
 
287
        else:
 
288
            match = {'message': [message_search]}
 
289
    return {
 
290
        'direction': direction,
 
291
        'specific_files': specific_files,
 
292
        'start_revision': start_revision,
 
293
        'end_revision': end_revision,
 
294
        'limit': limit,
 
295
        'levels': levels,
 
296
        'generate_tags': generate_tags,
 
297
        'delta_type': delta_type,
 
298
        'diff_type': diff_type,
 
299
        'exclude_common_ancestry': exclude_common_ancestry,
 
300
        'signature': signature,
 
301
        'match': match,
 
302
        'omit_merges': omit_merges,
 
303
        # Add 'private' attributes for features that may be deprecated
 
304
        '_match_using_deltas': _match_using_deltas,
 
305
    }
 
306
 
 
307
 
 
308
def _apply_log_request_defaults(rqst):
 
309
    """Apply default values to a request dictionary."""
 
310
    result = _DEFAULT_REQUEST_PARAMS.copy()
 
311
    if rqst:
 
312
        result.update(rqst)
 
313
    return result
 
314
 
 
315
 
 
316
def format_signature_validity(rev_id, branch):
 
317
    """get the signature validity
 
318
 
 
319
    :param rev_id: revision id to validate
 
320
    :param branch: branch of revision
 
321
    :return: human readable string to print to log
 
322
    """
 
323
    from breezy import gpg
 
324
 
 
325
    gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
 
326
    result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
 
327
    if result[0] == gpg.SIGNATURE_VALID:
 
328
        return u"valid signature from {0}".format(result[1])
 
329
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
330
        return "unknown key {0}".format(result[1])
 
331
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
332
        return "invalid signature!"
 
333
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
334
        return "no signature"
 
335
 
 
336
 
 
337
class LogGenerator(object):
 
338
    """A generator of log revisions."""
 
339
 
 
340
    def iter_log_revisions(self):
 
341
        """Iterate over LogRevision objects.
 
342
 
 
343
        :return: An iterator yielding LogRevision objects.
 
344
        """
 
345
        raise NotImplementedError(self.iter_log_revisions)
 
346
 
 
347
 
 
348
class Logger(object):
 
349
    """An object that generates, formats and displays a log."""
 
350
 
 
351
    def __init__(self, branch, rqst):
 
352
        """Create a Logger.
 
353
 
 
354
        :param branch: the branch to log
 
355
        :param rqst: A dictionary specifying the query parameters.
 
356
          See make_log_request_dict() for supported values.
 
357
        """
 
358
        self.branch = branch
 
359
        self.rqst = _apply_log_request_defaults(rqst)
 
360
 
 
361
    def show(self, lf):
 
362
        """Display the log.
 
363
 
 
364
        :param lf: The LogFormatter object to send the output to.
 
365
        """
 
366
        if not isinstance(lf, LogFormatter):
 
367
            warn("not a LogFormatter instance: %r" % lf)
 
368
 
 
369
        with self.branch.lock_read():
 
370
            if getattr(lf, 'begin_log', None):
 
371
                lf.begin_log()
 
372
            self._show_body(lf)
 
373
            if getattr(lf, 'end_log', None):
 
374
                lf.end_log()
 
375
 
 
376
    def _show_body(self, lf):
 
377
        """Show the main log output.
 
378
 
 
379
        Subclasses may wish to override this.
 
380
        """
 
381
        # Tweak the LogRequest based on what the LogFormatter can handle.
 
382
        # (There's no point generating stuff if the formatter can't display it.)
 
383
        rqst = self.rqst
 
384
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
385
            # user didn't specify levels, use whatever the LF can handle:
 
386
            rqst['levels'] = lf.get_levels()
 
387
 
 
388
        if not getattr(lf, 'supports_tags', False):
 
389
            rqst['generate_tags'] = False
 
390
        if not getattr(lf, 'supports_delta', False):
 
391
            rqst['delta_type'] = None
 
392
        if not getattr(lf, 'supports_diff', False):
 
393
            rqst['diff_type'] = None
 
394
        if not getattr(lf, 'supports_signatures', False):
 
395
            rqst['signature'] = False
 
396
 
 
397
        # Find and print the interesting revisions
 
398
        generator = self._generator_factory(self.branch, rqst)
 
399
        try:
 
400
            for lr in generator.iter_log_revisions():
 
401
                lf.log_revision(lr)
 
402
        except errors.GhostRevisionUnusableHere:
 
403
            raise errors.CommandError(
 
404
                gettext('Further revision history missing.'))
 
405
        lf.show_advice()
 
406
 
 
407
    def _generator_factory(self, branch, rqst):
 
408
        """Make the LogGenerator object to use.
 
409
 
 
410
        Subclasses may wish to override this.
 
411
        """
 
412
        return _DefaultLogGenerator(branch, **rqst)
 
413
 
 
414
 
 
415
def _log_revision_iterator_using_per_file_graph(
 
416
        branch, delta_type, match, levels, path, start_rev_id, end_rev_id,
 
417
        direction, exclude_common_ancestry):
 
418
    # Get the base revisions, filtering by the revision range.
 
419
    # Note that we always generate the merge revisions because
 
420
    # filter_revisions_touching_path() requires them ...
 
421
    view_revisions = _calc_view_revisions(
 
422
        branch, start_rev_id, end_rev_id,
 
423
        direction, generate_merge_revisions=True,
 
424
        exclude_common_ancestry=exclude_common_ancestry)
 
425
    if not isinstance(view_revisions, list):
 
426
        view_revisions = list(view_revisions)
 
427
    view_revisions = _filter_revisions_touching_path(
 
428
        branch, path, view_revisions,
 
429
        include_merges=levels != 1)
 
430
    return make_log_rev_iterator(
 
431
        branch, view_revisions, delta_type, match)
 
432
 
 
433
 
 
434
def _log_revision_iterator_using_delta_matching(
 
435
        branch, delta_type, match, levels, specific_files, start_rev_id, end_rev_id,
 
436
        direction, exclude_common_ancestry, limit):
 
437
    # Get the base revisions, filtering by the revision range
 
438
    generate_merge_revisions = levels != 1
 
439
    delayed_graph_generation = not specific_files and (
 
440
        limit or start_rev_id or end_rev_id)
 
441
    view_revisions = _calc_view_revisions(
 
442
        branch, start_rev_id, end_rev_id,
 
443
        direction,
 
444
        generate_merge_revisions=generate_merge_revisions,
 
445
        delayed_graph_generation=delayed_graph_generation,
 
446
        exclude_common_ancestry=exclude_common_ancestry)
 
447
 
 
448
    # Apply the other filters
 
449
    return make_log_rev_iterator(branch, view_revisions,
 
450
                                 delta_type, match,
 
451
                                 files=specific_files,
 
452
                                 direction=direction)
 
453
 
 
454
 
 
455
def _format_diff(branch, rev, diff_type, files=None):
 
456
    """Format a diff.
 
457
 
 
458
    :param branch: Branch object
 
459
    :param rev: Revision object
 
460
    :param diff_type: Type of diff to generate
 
461
    :param files: List of files to generate diff for (or None for all)
 
462
    """
 
463
    repo = branch.repository
 
464
    if len(rev.parent_ids) == 0:
 
465
        ancestor_id = _mod_revision.NULL_REVISION
 
466
    else:
 
467
        ancestor_id = rev.parent_ids[0]
 
468
    tree_1 = repo.revision_tree(ancestor_id)
 
469
    tree_2 = repo.revision_tree(rev.revision_id)
 
470
    if diff_type == 'partial' and files is not None:
 
471
        specific_files = files
 
472
    else:
 
473
        specific_files = None
 
474
    s = BytesIO()
 
475
    path_encoding = get_diff_header_encoding()
 
476
    diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
477
                         new_label='', path_encoding=path_encoding)
 
478
    return s.getvalue()
 
479
 
 
480
 
 
481
class _StartNotLinearAncestor(Exception):
 
482
    """Raised when a start revision is not found walking left-hand history."""
 
483
 
 
484
 
 
485
class _DefaultLogGenerator(LogGenerator):
 
486
    """The default generator of log revisions."""
 
487
 
 
488
    def __init__(
 
489
            self, branch, levels=None, limit=None, diff_type=None,
 
490
            delta_type=None, show_signature=None, omit_merges=None,
 
491
            generate_tags=None, specific_files=None, match=None,
 
492
            start_revision=None, end_revision=None, direction=None,
 
493
            exclude_common_ancestry=None, _match_using_deltas=None,
 
494
            signature=None):
 
495
        self.branch = branch
 
496
        self.levels = levels
 
497
        self.limit = limit
 
498
        self.diff_type = diff_type
 
499
        self.delta_type = delta_type
 
500
        self.show_signature = signature
 
501
        self.omit_merges = omit_merges
 
502
        self.specific_files = specific_files
 
503
        self.match = match
 
504
        self.start_revision = start_revision
 
505
        self.end_revision = end_revision
 
506
        self.direction = direction
 
507
        self.exclude_common_ancestry = exclude_common_ancestry
 
508
        self._match_using_deltas = _match_using_deltas
 
509
        if generate_tags and branch.supports_tags():
 
510
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
 
511
        else:
 
512
            self.rev_tag_dict = {}
 
513
 
 
514
    def iter_log_revisions(self):
 
515
        """Iterate over LogRevision objects.
 
516
 
 
517
        :return: An iterator yielding LogRevision objects.
 
518
        """
 
519
        log_count = 0
 
520
        revision_iterator = self._create_log_revision_iterator()
 
521
        for revs in revision_iterator:
 
522
            for (rev_id, revno, merge_depth), rev, delta in revs:
 
523
                # 0 levels means show everything; merge_depth counts from 0
 
524
                if (self.levels != 0 and merge_depth is not None and
 
525
                        merge_depth >= self.levels):
 
526
                    continue
 
527
                if self.omit_merges and len(rev.parent_ids) > 1:
 
528
                    continue
 
529
                if rev is None:
 
530
                    raise errors.GhostRevisionUnusableHere(rev_id)
 
531
                if self.diff_type is None:
 
532
                    diff = None
 
533
                else:
 
534
                    diff = _format_diff(
 
535
                        self.branch, rev, self.diff_type,
 
536
                        self.specific_files)
 
537
                if self.show_signature:
 
538
                    signature = format_signature_validity(rev_id, self.branch)
 
539
                else:
 
540
                    signature = None
 
541
                yield LogRevision(
 
542
                    rev, revno, merge_depth, delta,
 
543
                    self.rev_tag_dict.get(rev_id), diff, signature)
 
544
                if self.limit:
 
545
                    log_count += 1
 
546
                    if log_count >= self.limit:
 
547
                        return
 
548
 
 
549
    def _create_log_revision_iterator(self):
 
550
        """Create a revision iterator for log.
 
551
 
 
552
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
553
            delta).
 
554
        """
 
555
        start_rev_id, end_rev_id = _get_revision_limits(
 
556
            self.branch, self.start_revision, self.end_revision)
 
557
        if self._match_using_deltas:
 
558
            return _log_revision_iterator_using_delta_matching(
 
559
                self.branch,
 
560
                delta_type=self.delta_type,
 
561
                match=self.match,
 
562
                levels=self.levels,
 
563
                specific_files=self.specific_files,
 
564
                start_rev_id=start_rev_id, end_rev_id=end_rev_id,
 
565
                direction=self.direction,
 
566
                exclude_common_ancestry=self.exclude_common_ancestry,
 
567
                limit=self.limit)
 
568
        else:
 
569
            # We're using the per-file-graph algorithm. This scales really
 
570
            # well but only makes sense if there is a single file and it's
 
571
            # not a directory
 
572
            file_count = len(self.specific_files)
 
573
            if file_count != 1:
 
574
                raise errors.BzrError(
 
575
                    "illegal LogRequest: must match-using-deltas "
 
576
                    "when logging %d files" % file_count)
 
577
            return _log_revision_iterator_using_per_file_graph(
 
578
                self.branch,
 
579
                delta_type=self.delta_type,
 
580
                match=self.match,
 
581
                levels=self.levels,
 
582
                path=self.specific_files[0],
 
583
                start_rev_id=start_rev_id, end_rev_id=end_rev_id,
 
584
                direction=self.direction,
 
585
                exclude_common_ancestry=self.exclude_common_ancestry
 
586
                )
 
587
 
 
588
 
 
589
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
 
590
                         generate_merge_revisions,
 
591
                         delayed_graph_generation=False,
 
592
                         exclude_common_ancestry=False,
 
593
                         ):
 
594
    """Calculate the revisions to view.
 
595
 
 
596
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
 
597
             a list of the same tuples.
 
598
    """
 
599
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
 
600
        raise errors.CommandError(gettext(
 
601
            '--exclude-common-ancestry requires two different revisions'))
 
602
    if direction not in ('reverse', 'forward'):
 
603
        raise ValueError(gettext('invalid direction %r') % direction)
 
604
    br_rev_id = branch.last_revision()
 
605
    if br_rev_id == _mod_revision.NULL_REVISION:
 
606
        return []
 
607
 
 
608
    if (end_rev_id and start_rev_id == end_rev_id
 
609
        and (not generate_merge_revisions
 
610
             or not _has_merges(branch, end_rev_id))):
 
611
        # If a single revision is requested, check we can handle it
 
612
        return _generate_one_revision(branch, end_rev_id, br_rev_id,
 
613
                                      branch.revno())
 
614
    if not generate_merge_revisions:
 
615
        try:
 
616
            # If we only want to see linear revisions, we can iterate ...
 
617
            iter_revs = _linear_view_revisions(
 
618
                branch, start_rev_id, end_rev_id,
 
619
                exclude_common_ancestry=exclude_common_ancestry)
 
620
            # If a start limit was given and it's not obviously an
 
621
            # ancestor of the end limit, check it before outputting anything
 
622
            if (direction == 'forward'
 
623
                or (start_rev_id and not _is_obvious_ancestor(
 
624
                    branch, start_rev_id, end_rev_id))):
 
625
                iter_revs = list(iter_revs)
 
626
            if direction == 'forward':
 
627
                iter_revs = reversed(iter_revs)
 
628
            return iter_revs
 
629
        except _StartNotLinearAncestor:
 
630
            # Switch to the slower implementation that may be able to find a
 
631
            # non-obvious ancestor out of the left-hand history.
 
632
            pass
 
633
    iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
634
                                        direction, delayed_graph_generation,
 
635
                                        exclude_common_ancestry)
 
636
    if direction == 'forward':
 
637
        iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
638
    return iter_revs
 
639
 
 
640
 
 
641
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
 
642
    if rev_id == br_rev_id:
 
643
        # It's the tip
 
644
        return [(br_rev_id, br_revno, 0)]
 
645
    else:
 
646
        revno_str = _compute_revno_str(branch, rev_id)
 
647
        return [(rev_id, revno_str, 0)]
 
648
 
 
649
 
 
650
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
 
651
                            delayed_graph_generation,
 
652
                            exclude_common_ancestry=False):
 
653
    # On large trees, generating the merge graph can take 30-60 seconds
 
654
    # so we delay doing it until a merge is detected, incrementally
 
655
    # returning initial (non-merge) revisions while we can.
 
656
 
 
657
    # The above is only true for old formats (<= 0.92), for newer formats, a
 
658
    # couple of seconds only should be needed to load the whole graph and the
 
659
    # other graph operations needed are even faster than that -- vila 100201
 
660
    initial_revisions = []
 
661
    if delayed_graph_generation:
 
662
        try:
 
663
            for rev_id, revno, depth in _linear_view_revisions(
 
664
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
665
                if _has_merges(branch, rev_id):
 
666
                    # The end_rev_id can be nested down somewhere. We need an
 
667
                    # explicit ancestry check. There is an ambiguity here as we
 
668
                    # may not raise _StartNotLinearAncestor for a revision that
 
669
                    # is an ancestor but not a *linear* one. But since we have
 
670
                    # loaded the graph to do the check (or calculate a dotted
 
671
                    # revno), we may as well accept to show the log...  We need
 
672
                    # the check only if start_rev_id is not None as all
 
673
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
 
674
                    # -- vila 20100319
 
675
                    graph = branch.repository.get_graph()
 
676
                    if (start_rev_id is not None
 
677
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
678
                        raise _StartNotLinearAncestor()
 
679
                    # Since we collected the revisions so far, we need to
 
680
                    # adjust end_rev_id.
 
681
                    end_rev_id = rev_id
 
682
                    break
 
683
                else:
 
684
                    initial_revisions.append((rev_id, revno, depth))
 
685
            else:
 
686
                # No merged revisions found
 
687
                return initial_revisions
 
688
        except _StartNotLinearAncestor:
 
689
            # A merge was never detected so the lower revision limit can't
 
690
            # be nested down somewhere
 
691
            raise errors.CommandError(gettext('Start revision not found in'
 
692
                                                 ' history of end revision.'))
 
693
 
 
694
    # We exit the loop above because we encounter a revision with merges, from
 
695
    # this revision, we need to switch to _graph_view_revisions.
 
696
 
 
697
    # A log including nested merges is required. If the direction is reverse,
 
698
    # we rebase the initial merge depths so that the development line is
 
699
    # shown naturally, i.e. just like it is for linear logging. We can easily
 
700
    # make forward the exact opposite display, but showing the merge revisions
 
701
    # indented at the end seems slightly nicer in that case.
 
702
    view_revisions = itertools.chain(iter(initial_revisions),
 
703
                                     _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
704
                                                           rebase_initial_depths=(
 
705
                                                               direction == 'reverse'),
 
706
                                                           exclude_common_ancestry=exclude_common_ancestry))
 
707
    return view_revisions
 
708
 
 
709
 
 
710
def _has_merges(branch, rev_id):
 
711
    """Does a revision have multiple parents or not?"""
 
712
    parents = branch.repository.get_parent_map([rev_id]).get(rev_id, [])
 
713
    return len(parents) > 1
 
714
 
 
715
 
 
716
def _compute_revno_str(branch, rev_id):
 
717
    """Compute the revno string from a rev_id.
 
718
 
 
719
    :return: The revno string, or None if the revision is not in the supplied
 
720
        branch.
 
721
    """
 
722
    try:
 
723
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
724
    except errors.NoSuchRevision:
 
725
        # The revision must be outside of this branch
 
726
        return None
 
727
    else:
 
728
        return '.'.join(str(n) for n in revno)
 
729
 
 
730
 
 
731
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
 
732
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
 
733
    if start_rev_id and end_rev_id:
 
734
        try:
 
735
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
736
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
737
        except errors.NoSuchRevision:
 
738
            # one or both is not in the branch; not obvious
 
739
            return False
 
740
        if len(start_dotted) == 1 and len(end_dotted) == 1:
 
741
            # both on mainline
 
742
            return start_dotted[0] <= end_dotted[0]
 
743
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
 
744
              start_dotted[0:1] == end_dotted[0:1]):
 
745
            # both on same development line
 
746
            return start_dotted[2] <= end_dotted[2]
 
747
        else:
 
748
            # not obvious
 
749
            return False
 
750
    # if either start or end is not specified then we use either the first or
 
751
    # the last revision and *they* are obvious ancestors.
 
752
    return True
 
753
 
 
754
 
 
755
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
756
                           exclude_common_ancestry=False):
 
757
    """Calculate a sequence of revisions to view, newest to oldest.
 
758
 
 
759
    :param start_rev_id: the lower revision-id
 
760
    :param end_rev_id: the upper revision-id
 
761
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
762
        the iterated revisions.
 
763
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
764
        dotted_revno will be None for ghosts
 
765
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
 
766
        is not found walking the left-hand history
 
767
    """
 
768
    repo = branch.repository
 
769
    graph = repo.get_graph()
 
770
    if start_rev_id is None and end_rev_id is None:
 
771
        if branch._format.stores_revno() or \
 
772
                config.GlobalStack().get('calculate_revnos'):
 
773
            try:
 
774
                br_revno, br_rev_id = branch.last_revision_info()
 
775
            except errors.GhostRevisionsHaveNoRevno:
 
776
                br_rev_id = branch.last_revision()
 
777
                cur_revno = None
 
778
            else:
 
779
                cur_revno = br_revno
 
780
        else:
 
781
            br_rev_id = branch.last_revision()
 
782
            cur_revno = None
 
783
 
 
784
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
 
785
                                                  (_mod_revision.NULL_REVISION,))
 
786
        while True:
 
787
            try:
 
788
                revision_id = next(graph_iter)
 
789
            except errors.RevisionNotPresent as e:
 
790
                # Oops, a ghost.
 
791
                yield e.revision_id, None, None
 
792
                break
 
793
            except StopIteration:
 
794
                break
 
795
            else:
 
796
                yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
 
797
                if cur_revno is not None:
 
798
                    cur_revno -= 1
 
799
    else:
 
800
        br_rev_id = branch.last_revision()
 
801
        if end_rev_id is None:
 
802
            end_rev_id = br_rev_id
 
803
        found_start = start_rev_id is None
 
804
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
 
805
                                                  (_mod_revision.NULL_REVISION,))
 
806
        while True:
 
807
            try:
 
808
                revision_id = next(graph_iter)
 
809
            except StopIteration:
 
810
                break
 
811
            except errors.RevisionNotPresent as e:
 
812
                # Oops, a ghost.
 
813
                yield e.revision_id, None, None
 
814
                break
 
815
            else:
 
816
                revno_str = _compute_revno_str(branch, revision_id)
 
817
                if not found_start and revision_id == start_rev_id:
 
818
                    if not exclude_common_ancestry:
 
819
                        yield revision_id, revno_str, 0
 
820
                    found_start = True
 
821
                    break
 
822
                else:
 
823
                    yield revision_id, revno_str, 0
 
824
        if not found_start:
 
825
            raise _StartNotLinearAncestor()
 
826
 
 
827
 
 
828
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
829
                          rebase_initial_depths=True,
 
830
                          exclude_common_ancestry=False):
 
831
    """Calculate revisions to view including merges, newest to oldest.
 
832
 
 
833
    :param branch: the branch
 
834
    :param start_rev_id: the lower revision-id
 
835
    :param end_rev_id: the upper revision-id
 
836
    :param rebase_initial_depth: should depths be rebased until a mainline
 
837
      revision is found?
 
838
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
839
    """
 
840
    if exclude_common_ancestry:
 
841
        stop_rule = 'with-merges-without-common-ancestry'
 
842
    else:
 
843
        stop_rule = 'with-merges'
 
844
    view_revisions = branch.iter_merge_sorted_revisions(
 
845
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
 
846
        stop_rule=stop_rule)
 
847
    if not rebase_initial_depths:
 
848
        for (rev_id, merge_depth, revno, end_of_merge
 
849
             ) in view_revisions:
 
850
            yield rev_id, '.'.join(map(str, revno)), merge_depth
 
851
    else:
 
852
        # We're following a development line starting at a merged revision.
 
853
        # We need to adjust depths down by the initial depth until we find
 
854
        # a depth less than it. Then we use that depth as the adjustment.
 
855
        # If and when we reach the mainline, depth adjustment ends.
 
856
        depth_adjustment = None
 
857
        for (rev_id, merge_depth, revno, end_of_merge
 
858
             ) in view_revisions:
 
859
            if depth_adjustment is None:
 
860
                depth_adjustment = merge_depth
 
861
            if depth_adjustment:
 
862
                if merge_depth < depth_adjustment:
 
863
                    # From now on we reduce the depth adjustement, this can be
 
864
                    # surprising for users. The alternative requires two passes
 
865
                    # which breaks the fast display of the first revision
 
866
                    # though.
 
867
                    depth_adjustment = merge_depth
 
868
                merge_depth -= depth_adjustment
 
869
            yield rev_id, '.'.join(map(str, revno)), merge_depth
 
870
 
 
871
 
 
872
def _rebase_merge_depth(view_revisions):
 
873
    """Adjust depths upwards so the top level is 0."""
 
874
    # If either the first or last revision have a merge_depth of 0, we're done
 
875
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
 
876
        min_depth = min([d for r, n, d in view_revisions])
 
877
        if min_depth != 0:
 
878
            view_revisions = [(r, n, d - min_depth)
 
879
                              for r, n, d in view_revisions]
 
880
    return view_revisions
 
881
 
 
882
 
 
883
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
 
884
                          files=None, direction='reverse'):
 
885
    """Create a revision iterator for log.
 
886
 
 
887
    :param branch: The branch being logged.
 
888
    :param view_revisions: The revisions being viewed.
 
889
    :param generate_delta: Whether to generate a delta for each revision.
 
890
      Permitted values are None, 'full' and 'partial'.
 
891
    :param search: A user text search string.
 
892
    :param files: If non empty, only revisions matching one or more of
 
893
      the files are to be kept.
 
894
    :param direction: the direction in which view_revisions is sorted
 
895
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
896
        delta).
 
897
    """
 
898
    # Convert view_revisions into (view, None, None) groups to fit with
 
899
    # the standard interface here.
 
900
    if isinstance(view_revisions, list):
 
901
        # A single batch conversion is faster than many incremental ones.
 
902
        # As we have all the data, do a batch conversion.
 
903
        nones = [None] * len(view_revisions)
 
904
        log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
 
905
    else:
 
906
        def _convert():
 
907
            for view in view_revisions:
 
908
                yield (view, None, None)
 
909
        log_rev_iterator = iter([_convert()])
 
910
    for adapter in log_adapters:
 
911
        # It would be nicer if log adapters were first class objects
 
912
        # with custom parameters. This will do for now. IGC 20090127
 
913
        if adapter == _make_delta_filter:
 
914
            log_rev_iterator = adapter(
 
915
                branch, generate_delta, search, log_rev_iterator, files,
 
916
                direction)
 
917
        else:
 
918
            log_rev_iterator = adapter(
 
919
                branch, generate_delta, search, log_rev_iterator)
 
920
    return log_rev_iterator
 
921
 
 
922
 
 
923
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
 
924
    """Create a filtered iterator of log_rev_iterator matching on a regex.
 
925
 
 
926
    :param branch: The branch being logged.
 
927
    :param generate_delta: Whether to generate a delta for each revision.
 
928
    :param match: A dictionary with properties as keys and lists of strings
 
929
        as values. To match, a revision may match any of the supplied strings
 
930
        within a single property but must match at least one string for each
 
931
        property.
 
932
    :param log_rev_iterator: An input iterator containing all revisions that
 
933
        could be displayed, in lists.
 
934
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
935
        delta).
 
936
    """
 
937
    if not match:
 
938
        return log_rev_iterator
 
939
    # Use lazy_compile so mapping to InvalidPattern error occurs.
 
940
    searchRE = [(k, [lazy_regex.lazy_compile(x, re.IGNORECASE) for x in v])
 
941
                for k, v in match.items()]
 
942
    return _filter_re(searchRE, log_rev_iterator)
 
943
 
 
944
 
 
945
def _filter_re(searchRE, log_rev_iterator):
 
946
    for revs in log_rev_iterator:
 
947
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
948
        if new_revs:
 
949
            yield new_revs
 
950
 
 
951
 
 
952
def _match_filter(searchRE, rev):
 
953
    strings = {
 
954
        'message': (rev.message,),
 
955
        'committer': (rev.committer,),
 
956
        'author': (rev.get_apparent_authors()),
 
957
        'bugs': list(rev.iter_bugs())
 
958
        }
 
959
    strings[''] = [item for inner_list in strings.values()
 
960
                   for item in inner_list]
 
961
    for k, v in searchRE:
 
962
        if k in strings and not _match_any_filter(strings[k], v):
 
963
            return False
 
964
    return True
 
965
 
 
966
 
 
967
def _match_any_filter(strings, res):
 
968
    return any(r.search(s) for r in res for s in strings)
 
969
 
 
970
 
 
971
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
 
972
                       files=None, direction='reverse'):
 
973
    """Add revision deltas to a log iterator if needed.
 
974
 
 
975
    :param branch: The branch being logged.
 
976
    :param generate_delta: Whether to generate a delta for each revision.
 
977
      Permitted values are None, 'full' and 'partial'.
 
978
    :param search: A user text search string.
 
979
    :param log_rev_iterator: An input iterator containing all revisions that
 
980
        could be displayed, in lists.
 
981
    :param files: If non empty, only revisions matching one or more of
 
982
      the files are to be kept.
 
983
    :param direction: the direction in which view_revisions is sorted
 
984
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
985
        delta).
 
986
    """
 
987
    if not generate_delta and not files:
 
988
        return log_rev_iterator
 
989
    return _generate_deltas(branch.repository, log_rev_iterator,
 
990
                            generate_delta, files, direction)
 
991
 
 
992
 
 
993
def _generate_deltas(repository, log_rev_iterator, delta_type, files,
 
994
                     direction):
 
995
    """Create deltas for each batch of revisions in log_rev_iterator.
 
996
 
 
997
    If we're only generating deltas for the sake of filtering against
 
998
    files, we stop generating deltas once all files reach the
 
999
    appropriate life-cycle point. If we're receiving data newest to
 
1000
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
 
1001
    """
 
1002
    check_files = files is not None and len(files) > 0
 
1003
    if check_files:
 
1004
        file_set = set(files)
 
1005
        if direction == 'reverse':
 
1006
            stop_on = 'add'
 
1007
        else:
 
1008
            stop_on = 'remove'
 
1009
    else:
 
1010
        file_set = None
 
1011
    for revs in log_rev_iterator:
 
1012
        # If we were matching against files and we've run out,
 
1013
        # there's nothing left to do
 
1014
        if check_files and not file_set:
 
1015
            return
 
1016
        revisions = [rev[1] for rev in revs]
 
1017
        new_revs = []
 
1018
        if delta_type == 'full' and not check_files:
 
1019
            deltas = repository.get_revision_deltas(revisions)
 
1020
            for rev, delta in zip(revs, deltas):
 
1021
                new_revs.append((rev[0], rev[1], delta))
 
1022
        else:
 
1023
            deltas = repository.get_revision_deltas(
 
1024
                revisions, specific_files=file_set)
 
1025
            for rev, delta in zip(revs, deltas):
 
1026
                if check_files:
 
1027
                    if delta is None or not delta.has_changed():
 
1028
                        continue
 
1029
                    else:
 
1030
                        _update_files(delta, file_set, stop_on)
 
1031
                        if delta_type is None:
 
1032
                            delta = None
 
1033
                        elif delta_type == 'full':
 
1034
                            # If the file matches all the time, rebuilding
 
1035
                            # a full delta like this in addition to a partial
 
1036
                            # one could be slow. However, it's likely that
 
1037
                            # most revisions won't get this far, making it
 
1038
                            # faster to filter on the partial deltas and
 
1039
                            # build the occasional full delta than always
 
1040
                            # building full deltas and filtering those.
 
1041
                            rev_id = rev[0][0]
 
1042
                            delta = repository.get_revision_delta(rev_id)
 
1043
                new_revs.append((rev[0], rev[1], delta))
 
1044
        yield new_revs
 
1045
 
 
1046
 
 
1047
def _update_files(delta, files, stop_on):
 
1048
    """Update the set of files to search based on file lifecycle events.
 
1049
 
 
1050
    :param files: a set of files to update
 
1051
    :param stop_on: either 'add' or 'remove' - take files out of the
 
1052
      files set once their add or remove entry is detected respectively
 
1053
    """
 
1054
    if stop_on == 'add':
 
1055
        for item in delta.added:
 
1056
            if item.path[1] in files:
 
1057
                files.remove(item.path[1])
 
1058
        for item in delta.copied + delta.renamed:
 
1059
            if item.path[1] in files:
 
1060
                files.remove(item.path[1])
 
1061
                files.add(item.path[0])
 
1062
            if item.kind[1] == 'directory':
 
1063
                for path in list(files):
 
1064
                    if is_inside(item.path[1], path):
 
1065
                        files.remove(path)
 
1066
                        files.add(item.path[0] + path[len(item.path[1]):])
 
1067
    elif stop_on == 'delete':
 
1068
        for item in delta.removed:
 
1069
            if item.path[0] in files:
 
1070
                files.remove(item.path[0])
 
1071
        for item in delta.copied + delta.renamed:
 
1072
            if item.path[0] in files:
 
1073
                files.remove(item.path[0])
 
1074
                files.add(item.path[1])
 
1075
            if item.kind[0] == 'directory':
 
1076
                for path in list(files):
 
1077
                    if is_inside(item.path[0], path):
 
1078
                        files.remove(path)
 
1079
                        files.add(item.path[1] + path[len(item.path[0]):])
 
1080
 
 
1081
 
 
1082
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
 
1083
    """Extract revision objects from the repository
 
1084
 
 
1085
    :param branch: The branch being logged.
 
1086
    :param generate_delta: Whether to generate a delta for each revision.
 
1087
    :param search: A user text search string.
 
1088
    :param log_rev_iterator: An input iterator containing all revisions that
 
1089
        could be displayed, in lists.
 
1090
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
1091
        delta).
 
1092
    """
 
1093
    repository = branch.repository
 
1094
    for revs in log_rev_iterator:
 
1095
        # r = revision_id, n = revno, d = merge depth
 
1096
        revision_ids = [view[0] for view, _, _ in revs]
 
1097
        revisions = dict(repository.iter_revisions(revision_ids))
 
1098
        yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
 
1099
 
 
1100
 
 
1101
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
 
1102
    """Group up a single large batch into smaller ones.
 
1103
 
 
1104
    :param branch: The branch being logged.
 
1105
    :param generate_delta: Whether to generate a delta for each revision.
 
1106
    :param search: A user text search string.
 
1107
    :param log_rev_iterator: An input iterator containing all revisions that
 
1108
        could be displayed, in lists.
 
1109
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
1110
        delta).
 
1111
    """
 
1112
    num = 9
 
1113
    for batch in log_rev_iterator:
 
1114
        batch = iter(batch)
 
1115
        while True:
 
1116
            step = [detail for _, detail in zip(range(num), batch)]
 
1117
            if len(step) == 0:
 
1118
                break
 
1119
            yield step
 
1120
            num = min(int(num * 1.5), 200)
 
1121
 
 
1122
 
 
1123
def _get_revision_limits(branch, start_revision, end_revision):
 
1124
    """Get and check revision limits.
 
1125
 
 
1126
    :param branch: The branch containing the revisions.
 
1127
 
 
1128
    :param start_revision: The first revision to be logged, as a RevisionInfo.
 
1129
 
 
1130
    :param end_revision: The last revision to be logged, as a RevisionInfo
 
1131
 
 
1132
    :return: (start_rev_id, end_rev_id) tuple.
 
1133
    """
 
1134
    start_rev_id = None
 
1135
    start_revno = None
 
1136
    if start_revision is not None:
 
1137
        if not isinstance(start_revision, revisionspec.RevisionInfo):
 
1138
            raise TypeError(start_revision)
 
1139
        start_rev_id = start_revision.rev_id
 
1140
        start_revno = start_revision.revno
 
1141
    if start_revno is None:
 
1142
        start_revno = 1
 
1143
 
 
1144
    end_rev_id = None
 
1145
    end_revno = None
 
1146
    if end_revision is not None:
 
1147
        if not isinstance(end_revision, revisionspec.RevisionInfo):
 
1148
            raise TypeError(start_revision)
 
1149
        end_rev_id = end_revision.rev_id
 
1150
        end_revno = end_revision.revno
 
1151
 
 
1152
    if branch.last_revision() != _mod_revision.NULL_REVISION:
 
1153
        if (start_rev_id == _mod_revision.NULL_REVISION
 
1154
                or end_rev_id == _mod_revision.NULL_REVISION):
 
1155
            raise errors.CommandError(
 
1156
                gettext('Logging revision 0 is invalid.'))
 
1157
        if end_revno is not None and start_revno > end_revno:
 
1158
            raise errors.CommandError(
 
1159
                gettext("Start revision must be older than the end revision."))
 
1160
    return (start_rev_id, end_rev_id)
 
1161
 
 
1162
 
 
1163
def _get_mainline_revs(branch, start_revision, end_revision):
 
1164
    """Get the mainline revisions from the branch.
 
1165
 
 
1166
    Generates the list of mainline revisions for the branch.
 
1167
 
 
1168
    :param  branch: The branch containing the revisions.
 
1169
 
 
1170
    :param  start_revision: The first revision to be logged.
 
1171
            For backwards compatibility this may be a mainline integer revno,
 
1172
            but for merge revision support a RevisionInfo is expected.
 
1173
 
 
1174
    :param  end_revision: The last revision to be logged.
 
1175
            For backwards compatibility this may be a mainline integer revno,
 
1176
            but for merge revision support a RevisionInfo is expected.
 
1177
 
 
1178
    :return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
 
1179
    """
 
1180
    branch_revno, branch_last_revision = branch.last_revision_info()
 
1181
    if branch_revno == 0:
 
1182
        return None, None, None, None
 
1183
 
 
1184
    # For mainline generation, map start_revision and end_revision to
 
1185
    # mainline revnos. If the revision is not on the mainline choose the
 
1186
    # appropriate extreme of the mainline instead - the extra will be
 
1187
    # filtered later.
 
1188
    # Also map the revisions to rev_ids, to be used in the later filtering
 
1189
    # stage.
 
1190
    start_rev_id = None
162
1191
    if start_revision is None:
163
 
        start_revision = 1
 
1192
        start_revno = 1
164
1193
    else:
165
 
        branch.check_real_revno(start_revision)
166
 
    
 
1194
        if isinstance(start_revision, revisionspec.RevisionInfo):
 
1195
            start_rev_id = start_revision.rev_id
 
1196
            start_revno = start_revision.revno or 1
 
1197
        else:
 
1198
            branch.check_real_revno(start_revision)
 
1199
            start_revno = start_revision
 
1200
 
 
1201
    end_rev_id = None
167
1202
    if end_revision is None:
168
 
        end_revision = len(which_revs)
169
 
    else:
170
 
        branch.check_real_revno(end_revision)
171
 
 
172
 
    # list indexes are 0-based; revisions are 1-based
173
 
    cut_revs = which_revs[(start_revision-1):(end_revision)]
174
 
 
175
 
    if direction == 'reverse':
176
 
        cut_revs.reverse()
177
 
    elif direction == 'forward':
178
 
        pass
179
 
    else:
180
 
        raise ValueError('invalid direction %r' % direction)
181
 
 
182
 
    for revno, rev_id in cut_revs:
183
 
        if verbose or specific_fileid:
184
 
            delta = branch.get_revision_delta(revno)
185
 
            
186
 
        if specific_fileid:
187
 
            if not delta.touches_file_id(specific_fileid):
188
 
                continue
189
 
 
190
 
        if not verbose:
191
 
            # although we calculated it, throw it away without display
192
 
            delta = None
193
 
 
194
 
        rev = branch.get_revision(rev_id)
195
 
 
196
 
        if searchRE:
197
 
            if not searchRE.search(rev.message):
198
 
                continue
199
 
 
200
 
        lf.show(revno, rev, delta)
201
 
 
202
 
 
203
 
 
204
 
def deltas_for_log_dummy(branch, which_revs):
205
 
    """Return all the revisions without intermediate deltas.
206
 
 
207
 
    Useful for log commands that won't need the delta information.
208
 
    """
209
 
    
210
 
    for revno, revision_id in which_revs:
211
 
        yield revno, branch.get_revision(revision_id), None
212
 
 
213
 
 
214
 
def deltas_for_log_reverse(branch, which_revs):
215
 
    """Compute deltas for display in latest-to-earliest order.
216
 
 
217
 
    branch
218
 
        Branch to traverse
219
 
 
220
 
    which_revs
221
 
        Sequence of (revno, revision_id) for the subset of history to examine
222
 
 
223
 
    returns 
224
 
        Sequence of (revno, rev, delta)
225
 
 
226
 
    The delta is from the given revision to the next one in the
227
 
    sequence, which makes sense if the log is being displayed from
228
 
    newest to oldest.
229
 
    """
230
 
    last_revno = last_revision_id = last_tree = None
231
 
    for revno, revision_id in which_revs:
232
 
        this_tree = branch.revision_tree(revision_id)
233
 
        this_revision = branch.get_revision(revision_id)
234
 
        
235
 
        if last_revno:
236
 
            yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
237
 
 
238
 
        this_tree = EmptyTree(branch.get_root_id())
239
 
 
240
 
        last_revno = revno
241
 
        last_revision = this_revision
242
 
        last_tree = this_tree
243
 
 
244
 
    if last_revno:
245
 
        if last_revno == 1:
246
 
            this_tree = EmptyTree(branch.get_root_id())
247
 
        else:
248
 
            this_revno = last_revno - 1
249
 
            this_revision_id = branch.revision_history()[this_revno]
250
 
            this_tree = branch.revision_tree(this_revision_id)
251
 
        yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
252
 
 
253
 
 
254
 
def deltas_for_log_forward(branch, which_revs):
255
 
    """Compute deltas for display in forward log.
256
 
 
257
 
    Given a sequence of (revno, revision_id) pairs, return
258
 
    (revno, rev, delta).
259
 
 
260
 
    The delta is from the given revision to the next one in the
261
 
    sequence, which makes sense if the log is being displayed from
262
 
    newest to oldest.
263
 
    """
264
 
    last_revno = last_revision_id = last_tree = None
265
 
    prev_tree = EmptyTree(branch.get_root_id())
266
 
 
267
 
    for revno, revision_id in which_revs:
268
 
        this_tree = branch.revision_tree(revision_id)
269
 
        this_revision = branch.get_revision(revision_id)
270
 
 
271
 
        if not last_revno:
272
 
            if revno == 1:
273
 
                last_tree = EmptyTree(branch.get_root_id())
274
 
            else:
275
 
                last_revno = revno - 1
276
 
                last_revision_id = branch.revision_history()[last_revno]
277
 
                last_tree = branch.revision_tree(last_revision_id)
278
 
 
279
 
        yield revno, this_revision, compare_trees(last_tree, this_tree, False)
280
 
 
281
 
        last_revno = revno
282
 
        last_revision = this_revision
283
 
        last_tree = this_tree
 
1203
        end_revno = branch_revno
 
1204
    else:
 
1205
        if isinstance(end_revision, revisionspec.RevisionInfo):
 
1206
            end_rev_id = end_revision.rev_id
 
1207
            end_revno = end_revision.revno or branch_revno
 
1208
        else:
 
1209
            branch.check_real_revno(end_revision)
 
1210
            end_revno = end_revision
 
1211
 
 
1212
    if ((start_rev_id == _mod_revision.NULL_REVISION)
 
1213
            or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1214
        raise errors.CommandError(gettext('Logging revision 0 is invalid.'))
 
1215
    if start_revno > end_revno:
 
1216
        raise errors.CommandError(gettext("Start revision must be older "
 
1217
                                             "than the end revision."))
 
1218
 
 
1219
    if end_revno < start_revno:
 
1220
        return None, None, None, None
 
1221
    cur_revno = branch_revno
 
1222
    rev_nos = {}
 
1223
    mainline_revs = []
 
1224
    graph = branch.repository.get_graph()
 
1225
    for revision_id in graph.iter_lefthand_ancestry(
 
1226
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
 
1227
        if cur_revno < start_revno:
 
1228
            # We have gone far enough, but we always add 1 more revision
 
1229
            rev_nos[revision_id] = cur_revno
 
1230
            mainline_revs.append(revision_id)
 
1231
            break
 
1232
        if cur_revno <= end_revno:
 
1233
            rev_nos[revision_id] = cur_revno
 
1234
            mainline_revs.append(revision_id)
 
1235
        cur_revno -= 1
 
1236
    else:
 
1237
        # We walked off the edge of all revisions, so we add a 'None' marker
 
1238
        mainline_revs.append(None)
 
1239
 
 
1240
    mainline_revs.reverse()
 
1241
 
 
1242
    # override the mainline to look like the revision history.
 
1243
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
 
1244
 
 
1245
 
 
1246
def _filter_revisions_touching_path(branch, path, view_revisions,
 
1247
                                    include_merges=True):
 
1248
    r"""Return the list of revision ids which touch a given path.
 
1249
 
 
1250
    The function filters view_revisions and returns a subset.
 
1251
    This includes the revisions which directly change the path,
 
1252
    and the revisions which merge these changes. So if the
 
1253
    revision graph is::
 
1254
 
 
1255
        A-.
 
1256
        |\ \
 
1257
        B C E
 
1258
        |/ /
 
1259
        D |
 
1260
        |\|
 
1261
        | F
 
1262
        |/
 
1263
        G
 
1264
 
 
1265
    And 'C' changes a file, then both C and D will be returned. F will not be
 
1266
    returned even though it brings the changes to C into the branch starting
 
1267
    with E. (Note that if we were using F as the tip instead of G, then we
 
1268
    would see C, D, F.)
 
1269
 
 
1270
    This will also be restricted based on a subset of the mainline.
 
1271
 
 
1272
    :param branch: The branch where we can get text revision information.
 
1273
 
 
1274
    :param path: Filter out revisions that do not touch path.
 
1275
 
 
1276
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
 
1277
        tuples. This is the list of revisions which will be filtered. It is
 
1278
        assumed that view_revisions is in merge_sort order (i.e. newest
 
1279
        revision first ).
 
1280
 
 
1281
    :param include_merges: include merge revisions in the result or not
 
1282
 
 
1283
    :return: A list of (revision_id, dotted_revno, merge_depth) tuples.
 
1284
    """
 
1285
    # Lookup all possible text keys to determine which ones actually modified
 
1286
    # the file.
 
1287
    graph = branch.repository.get_file_graph()
 
1288
    start_tree = branch.repository.revision_tree(view_revisions[0][0])
 
1289
    file_id = start_tree.path2id(path)
 
1290
    get_parent_map = graph.get_parent_map
 
1291
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
 
1292
    next_keys = None
 
1293
    # Looking up keys in batches of 1000 can cut the time in half, as well as
 
1294
    # memory consumption. GraphIndex *does* like to look for a few keys in
 
1295
    # parallel, it just doesn't like looking for *lots* of keys in parallel.
 
1296
    # TODO: This code needs to be re-evaluated periodically as we tune the
 
1297
    #       indexing layer. We might consider passing in hints as to the known
 
1298
    #       access pattern (sparse/clustered, high success rate/low success
 
1299
    #       rate). This particular access is clustered with a low success rate.
 
1300
    modified_text_revisions = set()
 
1301
    chunk_size = 1000
 
1302
    for start in range(0, len(text_keys), chunk_size):
 
1303
        next_keys = text_keys[start:start + chunk_size]
 
1304
        # Only keep the revision_id portion of the key
 
1305
        modified_text_revisions.update(
 
1306
            [k[1] for k in get_parent_map(next_keys)])
 
1307
    del text_keys, next_keys
 
1308
 
 
1309
    result = []
 
1310
    # Track what revisions will merge the current revision, replace entries
 
1311
    # with 'None' when they have been added to result
 
1312
    current_merge_stack = [None]
 
1313
    for info in view_revisions:
 
1314
        rev_id, revno, depth = info
 
1315
        if depth == len(current_merge_stack):
 
1316
            current_merge_stack.append(info)
 
1317
        else:
 
1318
            del current_merge_stack[depth + 1:]
 
1319
            current_merge_stack[-1] = info
 
1320
 
 
1321
        if rev_id in modified_text_revisions:
 
1322
            # This needs to be logged, along with the extra revisions
 
1323
            for idx in range(len(current_merge_stack)):
 
1324
                node = current_merge_stack[idx]
 
1325
                if node is not None:
 
1326
                    if include_merges or node[2] == 0:
 
1327
                        result.append(node)
 
1328
                        current_merge_stack[idx] = None
 
1329
    return result
 
1330
 
 
1331
 
 
1332
def reverse_by_depth(merge_sorted_revisions, _depth=0):
 
1333
    """Reverse revisions by depth.
 
1334
 
 
1335
    Revisions with a different depth are sorted as a group with the previous
 
1336
    revision of that depth.  There may be no topological justification for this
 
1337
    but it looks much nicer.
 
1338
    """
 
1339
    # Add a fake revision at start so that we can always attach sub revisions
 
1340
    merge_sorted_revisions = [(None, None, _depth)] + merge_sorted_revisions
 
1341
    zd_revisions = []
 
1342
    for val in merge_sorted_revisions:
 
1343
        if val[2] == _depth:
 
1344
            # Each revision at the current depth becomes a chunk grouping all
 
1345
            # higher depth revisions.
 
1346
            zd_revisions.append([val])
 
1347
        else:
 
1348
            zd_revisions[-1].append(val)
 
1349
    for revisions in zd_revisions:
 
1350
        if len(revisions) > 1:
 
1351
            # We have higher depth revisions, let reverse them locally
 
1352
            revisions[1:] = reverse_by_depth(revisions[1:], _depth + 1)
 
1353
    zd_revisions.reverse()
 
1354
    result = []
 
1355
    for chunk in zd_revisions:
 
1356
        result.extend(chunk)
 
1357
    if _depth == 0:
 
1358
        # Top level call, get rid of the fake revisions that have been added
 
1359
        result = [r for r in result if r[0] is not None and r[1] is not None]
 
1360
    return result
 
1361
 
 
1362
 
 
1363
class LogRevision(object):
 
1364
    """A revision to be logged (by LogFormatter.log_revision).
 
1365
 
 
1366
    A simple wrapper for the attributes of a revision to be logged.
 
1367
    The attributes may or may not be populated, as determined by the
 
1368
    logging options and the log formatter capabilities.
 
1369
    """
 
1370
 
 
1371
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
 
1372
                 tags=None, diff=None, signature=None):
 
1373
        self.rev = rev
 
1374
        if revno is None:
 
1375
            self.revno = None
 
1376
        else:
 
1377
            self.revno = str(revno)
 
1378
        self.merge_depth = merge_depth
 
1379
        self.delta = delta
 
1380
        self.tags = tags
 
1381
        self.diff = diff
 
1382
        self.signature = signature
284
1383
 
285
1384
 
286
1385
class LogFormatter(object):
287
 
    """Abstract class to display log messages."""
288
 
    def __init__(self, to_file, show_ids=False, show_timezone='original'):
 
1386
    """Abstract class to display log messages.
 
1387
 
 
1388
    At a minimum, a derived class must implement the log_revision method.
 
1389
 
 
1390
    If the LogFormatter needs to be informed of the beginning or end of
 
1391
    a log it should implement the begin_log and/or end_log hook methods.
 
1392
 
 
1393
    A LogFormatter should define the following supports_XXX flags
 
1394
    to indicate which LogRevision attributes it supports:
 
1395
 
 
1396
    - supports_delta must be True if this log formatter supports delta.
 
1397
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1398
      attribute describes whether the 'short_status' format (1) or the long
 
1399
      one (2) should be used.
 
1400
 
 
1401
    - supports_merge_revisions must be True if this log formatter supports
 
1402
      merge revisions.  If not, then only mainline revisions will be passed
 
1403
      to the formatter.
 
1404
 
 
1405
    - preferred_levels is the number of levels this formatter defaults to.
 
1406
      The default value is zero meaning display all levels.
 
1407
      This value is only relevant if supports_merge_revisions is True.
 
1408
 
 
1409
    - supports_tags must be True if this log formatter supports tags.
 
1410
      Otherwise the tags attribute may not be populated.
 
1411
 
 
1412
    - supports_diff must be True if this log formatter supports diffs.
 
1413
      Otherwise the diff attribute may not be populated.
 
1414
 
 
1415
    - supports_signatures must be True if this log formatter supports GPG
 
1416
      signatures.
 
1417
 
 
1418
    Plugins can register functions to show custom revision properties using
 
1419
    the properties_handler_registry. The registered function
 
1420
    must respect the following interface description::
 
1421
 
 
1422
        def my_show_properties(properties_dict):
 
1423
            # code that returns a dict {'name':'value'} of the properties
 
1424
            # to be shown
 
1425
    """
 
1426
    preferred_levels = 0
 
1427
 
 
1428
    def __init__(self, to_file, show_ids=False, show_timezone='original',
 
1429
                 delta_format=None, levels=None, show_advice=False,
 
1430
                 to_exact_file=None, author_list_handler=None):
 
1431
        """Create a LogFormatter.
 
1432
 
 
1433
        :param to_file: the file to output to
 
1434
        :param to_exact_file: if set, gives an output stream to which
 
1435
             non-Unicode diffs are written.
 
1436
        :param show_ids: if True, revision-ids are to be displayed
 
1437
        :param show_timezone: the timezone to use
 
1438
        :param delta_format: the level of delta information to display
 
1439
          or None to leave it to the formatter to decide
 
1440
        :param levels: the number of levels to display; None or -1 to
 
1441
          let the log formatter decide.
 
1442
        :param show_advice: whether to show advice at the end of the
 
1443
          log or not
 
1444
        :param author_list_handler: callable generating a list of
 
1445
          authors to display for a given revision
 
1446
        """
289
1447
        self.to_file = to_file
 
1448
        # 'exact' stream used to show diff, it should print content 'as is'
 
1449
        # and should not try to decode/encode it to unicode to avoid bug
 
1450
        # #328007
 
1451
        if to_exact_file is not None:
 
1452
            self.to_exact_file = to_exact_file
 
1453
        else:
 
1454
            # XXX: somewhat hacky; this assumes it's a codec writer; it's
 
1455
            # better for code that expects to get diffs to pass in the exact
 
1456
            # file stream
 
1457
            self.to_exact_file = getattr(to_file, 'stream', to_file)
290
1458
        self.show_ids = show_ids
291
1459
        self.show_timezone = show_timezone
292
 
 
293
 
 
294
 
    def show(self, revno, rev, delta):
 
1460
        if delta_format is None:
 
1461
            # Ensures backward compatibility
 
1462
            delta_format = 2  # long format
 
1463
        self.delta_format = delta_format
 
1464
        self.levels = levels
 
1465
        self._show_advice = show_advice
 
1466
        self._merge_count = 0
 
1467
        self._author_list_handler = author_list_handler
 
1468
 
 
1469
    def get_levels(self):
 
1470
        """Get the number of levels to display or 0 for all."""
 
1471
        if getattr(self, 'supports_merge_revisions', False):
 
1472
            if self.levels is None or self.levels == -1:
 
1473
                self.levels = self.preferred_levels
 
1474
        else:
 
1475
            self.levels = 1
 
1476
        return self.levels
 
1477
 
 
1478
    def log_revision(self, revision):
 
1479
        """Log a revision.
 
1480
 
 
1481
        :param  revision:   The LogRevision to be logged.
 
1482
        """
295
1483
        raise NotImplementedError('not implemented in abstract base')
296
 
        
297
 
 
298
 
 
299
 
 
 
1484
 
 
1485
    def show_advice(self):
 
1486
        """Output user advice, if any, when the log is completed."""
 
1487
        if self._show_advice and self.levels == 1 and self._merge_count > 0:
 
1488
            advice_sep = self.get_advice_separator()
 
1489
            if advice_sep:
 
1490
                self.to_file.write(advice_sep)
 
1491
            self.to_file.write(
 
1492
                "Use --include-merged or -n0 to see merged revisions.\n")
 
1493
 
 
1494
    def get_advice_separator(self):
 
1495
        """Get the text separating the log from the closing advice."""
 
1496
        return ''
 
1497
 
 
1498
    def short_committer(self, rev):
 
1499
        name, address = config.parse_username(rev.committer)
 
1500
        if name:
 
1501
            return name
 
1502
        return address
 
1503
 
 
1504
    def short_author(self, rev):
 
1505
        return self.authors(rev, 'first', short=True, sep=', ')
 
1506
 
 
1507
    def authors(self, rev, who, short=False, sep=None):
 
1508
        """Generate list of authors, taking --authors option into account.
 
1509
 
 
1510
        The caller has to specify the name of a author list handler,
 
1511
        as provided by the author list registry, using the ``who``
 
1512
        argument.  That name only sets a default, though: when the
 
1513
        user selected a different author list generation using the
 
1514
        ``--authors`` command line switch, as represented by the
 
1515
        ``author_list_handler`` constructor argument, that value takes
 
1516
        precedence.
 
1517
 
 
1518
        :param rev: The revision for which to generate the list of authors.
 
1519
        :param who: Name of the default handler.
 
1520
        :param short: Whether to shorten names to either name or address.
 
1521
        :param sep: What separator to use for automatic concatenation.
 
1522
        """
 
1523
        if self._author_list_handler is not None:
 
1524
            # The user did specify --authors, which overrides the default
 
1525
            author_list_handler = self._author_list_handler
 
1526
        else:
 
1527
            # The user didn't specify --authors, so we use the caller's default
 
1528
            author_list_handler = author_list_registry.get(who)
 
1529
        names = author_list_handler(rev)
 
1530
        if short:
 
1531
            for i in range(len(names)):
 
1532
                name, address = config.parse_username(names[i])
 
1533
                if name:
 
1534
                    names[i] = name
 
1535
                else:
 
1536
                    names[i] = address
 
1537
        if sep is not None:
 
1538
            names = sep.join(names)
 
1539
        return names
 
1540
 
 
1541
    def merge_marker(self, revision):
 
1542
        """Get the merge marker to include in the output or '' if none."""
 
1543
        if len(revision.rev.parent_ids) > 1:
 
1544
            self._merge_count += 1
 
1545
            return ' [merge]'
 
1546
        else:
 
1547
            return ''
 
1548
 
 
1549
    def show_properties(self, revision, indent):
 
1550
        """Displays the custom properties returned by each registered handler.
 
1551
 
 
1552
        If a registered handler raises an error it is propagated.
 
1553
        """
 
1554
        for line in self.custom_properties(revision):
 
1555
            self.to_file.write("%s%s\n" % (indent, line))
 
1556
 
 
1557
    def custom_properties(self, revision):
 
1558
        """Format the custom properties returned by each registered handler.
 
1559
 
 
1560
        If a registered handler raises an error it is propagated.
 
1561
 
 
1562
        :return: a list of formatted lines (excluding trailing newlines)
 
1563
        """
 
1564
        lines = self._foreign_info_properties(revision)
 
1565
        for key, handler in properties_handler_registry.iteritems():
 
1566
            try:
 
1567
                lines.extend(self._format_properties(handler(revision)))
 
1568
            except Exception:
 
1569
                trace.log_exception_quietly()
 
1570
                trace.print_exception(sys.exc_info(), self.to_file)
 
1571
        return lines
 
1572
 
 
1573
    def _foreign_info_properties(self, rev):
 
1574
        """Custom log displayer for foreign revision identifiers.
 
1575
 
 
1576
        :param rev: Revision object.
 
1577
        """
 
1578
        # Revision comes directly from a foreign repository
 
1579
        if isinstance(rev, foreign.ForeignRevision):
 
1580
            return self._format_properties(
 
1581
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1582
 
 
1583
        # Imported foreign revision revision ids always contain :
 
1584
        if b":" not in rev.revision_id:
 
1585
            return []
 
1586
 
 
1587
        # Revision was once imported from a foreign repository
 
1588
        try:
 
1589
            foreign_revid, mapping = \
 
1590
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
 
1591
        except errors.InvalidRevisionId:
 
1592
            return []
 
1593
 
 
1594
        return self._format_properties(
 
1595
            mapping.vcs.show_foreign_revid(foreign_revid))
 
1596
 
 
1597
    def _format_properties(self, properties):
 
1598
        lines = []
 
1599
        for key, value in properties.items():
 
1600
            lines.append(key + ': ' + value)
 
1601
        return lines
 
1602
 
 
1603
    def show_diff(self, to_file, diff, indent):
 
1604
        encoding = get_terminal_encoding()
 
1605
        for l in diff.rstrip().split(b'\n'):
 
1606
            to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
 
1607
 
 
1608
 
 
1609
# Separator between revisions in long format
 
1610
_LONG_SEP = '-' * 60
300
1611
 
301
1612
 
302
1613
class LongLogFormatter(LogFormatter):
303
 
    def show(self, revno, rev, delta):
304
 
        from osutils import format_date
305
 
 
 
1614
 
 
1615
    supports_merge_revisions = True
 
1616
    preferred_levels = 1
 
1617
    supports_delta = True
 
1618
    supports_tags = True
 
1619
    supports_diff = True
 
1620
    supports_signatures = True
 
1621
 
 
1622
    def __init__(self, *args, **kwargs):
 
1623
        super(LongLogFormatter, self).__init__(*args, **kwargs)
 
1624
        if self.show_timezone == 'original':
 
1625
            self.date_string = self._date_string_original_timezone
 
1626
        else:
 
1627
            self.date_string = self._date_string_with_timezone
 
1628
 
 
1629
    def _date_string_with_timezone(self, rev):
 
1630
        return format_date(rev.timestamp, rev.timezone or 0,
 
1631
                           self.show_timezone)
 
1632
 
 
1633
    def _date_string_original_timezone(self, rev):
 
1634
        return format_date_with_offset_in_original_timezone(rev.timestamp,
 
1635
                                                            rev.timezone or 0)
 
1636
 
 
1637
    def log_revision(self, revision):
 
1638
        """Log a revision, either merged or not."""
 
1639
        indent = '    ' * revision.merge_depth
 
1640
        lines = [_LONG_SEP]
 
1641
        if revision.revno is not None:
 
1642
            lines.append('revno: %s%s' % (revision.revno,
 
1643
                                          self.merge_marker(revision)))
 
1644
        if revision.tags:
 
1645
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
 
1646
        if self.show_ids or revision.revno is None:
 
1647
            lines.append('revision-id: %s' %
 
1648
                         (revision.rev.revision_id.decode('utf-8'),))
 
1649
        if self.show_ids:
 
1650
            for parent_id in revision.rev.parent_ids:
 
1651
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
 
1652
        lines.extend(self.custom_properties(revision.rev))
 
1653
 
 
1654
        committer = revision.rev.committer
 
1655
        authors = self.authors(revision.rev, 'all')
 
1656
        if authors != [committer]:
 
1657
            lines.append('author: %s' % (", ".join(authors),))
 
1658
        lines.append('committer: %s' % (committer,))
 
1659
 
 
1660
        branch_nick = revision.rev.properties.get('branch-nick', None)
 
1661
        if branch_nick is not None:
 
1662
            lines.append('branch nick: %s' % (branch_nick,))
 
1663
 
 
1664
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
 
1665
 
 
1666
        if revision.signature is not None:
 
1667
            lines.append('signature: ' + revision.signature)
 
1668
 
 
1669
        lines.append('message:')
 
1670
        if not revision.rev.message:
 
1671
            lines.append('  (no message)')
 
1672
        else:
 
1673
            message = revision.rev.message.rstrip('\r\n')
 
1674
            for l in message.split('\n'):
 
1675
                lines.append('  %s' % (l,))
 
1676
 
 
1677
        # Dump the output, appending the delta and diff if requested
306
1678
        to_file = self.to_file
307
 
 
308
 
        print >>to_file,  '-' * 60
309
 
        print >>to_file,  'revno:', revno
310
 
        if self.show_ids:
311
 
            print >>to_file,  'revision-id:', rev.revision_id
312
 
 
313
 
            for parent in rev.parents:
314
 
                print >>to_file, 'parent:', parent.revision_id
315
 
            
316
 
        print >>to_file,  'committer:', rev.committer
317
 
 
318
 
        date_str = format_date(rev.timestamp,
319
 
                               rev.timezone or 0,
320
 
                               self.show_timezone)
321
 
        print >>to_file,  'timestamp: %s' % date_str
322
 
 
323
 
        print >>to_file,  'message:'
324
 
        if not rev.message:
325
 
            print >>to_file,  '  (no message)'
326
 
        else:
327
 
            for l in rev.message.split('\n'):
328
 
                print >>to_file,  '  ' + l
329
 
 
330
 
        if delta != None:
331
 
            delta.show(to_file, self.show_ids)
332
 
 
 
1679
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
 
1680
        if revision.delta is not None:
 
1681
            # Use the standard status output to display changes
 
1682
            from breezy.delta import report_delta
 
1683
            report_delta(to_file, revision.delta, short_status=False,
 
1684
                         show_ids=self.show_ids, indent=indent)
 
1685
        if revision.diff is not None:
 
1686
            to_file.write(indent + 'diff:\n')
 
1687
            to_file.flush()
 
1688
            # Note: we explicitly don't indent the diff (relative to the
 
1689
            # revision information) so that the output can be fed to patch -p0
 
1690
            self.show_diff(self.to_exact_file, revision.diff, indent)
 
1691
            self.to_exact_file.flush()
 
1692
 
 
1693
    def get_advice_separator(self):
 
1694
        """Get the text separating the log from the closing advice."""
 
1695
        return '-' * 60 + '\n'
333
1696
 
334
1697
 
335
1698
class ShortLogFormatter(LogFormatter):
336
 
    def show(self, revno, rev, delta):
337
 
        from bzrlib.osutils import format_date
 
1699
 
 
1700
    supports_merge_revisions = True
 
1701
    preferred_levels = 1
 
1702
    supports_delta = True
 
1703
    supports_tags = True
 
1704
    supports_diff = True
 
1705
 
 
1706
    def __init__(self, *args, **kwargs):
 
1707
        super(ShortLogFormatter, self).__init__(*args, **kwargs)
 
1708
        self.revno_width_by_depth = {}
 
1709
 
 
1710
    def log_revision(self, revision):
 
1711
        # We need two indents: one per depth and one for the information
 
1712
        # relative to that indent. Most mainline revnos are 5 chars or
 
1713
        # less while dotted revnos are typically 11 chars or less. Once
 
1714
        # calculated, we need to remember the offset for a given depth
 
1715
        # as we might be starting from a dotted revno in the first column
 
1716
        # and we want subsequent mainline revisions to line up.
 
1717
        depth = revision.merge_depth
 
1718
        indent = '    ' * depth
 
1719
        revno_width = self.revno_width_by_depth.get(depth)
 
1720
        if revno_width is None:
 
1721
            if revision.revno is None or revision.revno.find('.') == -1:
 
1722
                # mainline revno, e.g. 12345
 
1723
                revno_width = 5
 
1724
            else:
 
1725
                # dotted revno, e.g. 12345.10.55
 
1726
                revno_width = 11
 
1727
            self.revno_width_by_depth[depth] = revno_width
 
1728
        offset = ' ' * (revno_width + 1)
338
1729
 
339
1730
        to_file = self.to_file
340
 
 
341
 
        print >>to_file, "%5d %s\t%s" % (revno, rev.committer,
342
 
                format_date(rev.timestamp, rev.timezone or 0,
343
 
                            self.show_timezone))
344
 
        if self.show_ids:
345
 
            print >>to_file,  '      revision-id:', rev.revision_id
 
1731
        tags = ''
 
1732
        if revision.tags:
 
1733
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
 
1734
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
 
1735
                                                     revision.revno or "", self.short_author(
 
1736
                                                         revision.rev),
 
1737
                                                     format_date(revision.rev.timestamp,
 
1738
                                                                 revision.rev.timezone or 0,
 
1739
                                                                 self.show_timezone, date_fmt="%Y-%m-%d",
 
1740
                                                                 show_offset=False),
 
1741
                                                     tags, self.merge_marker(revision)))
 
1742
        self.show_properties(revision.rev, indent + offset)
 
1743
        if self.show_ids or revision.revno is None:
 
1744
            to_file.write(indent + offset + 'revision-id:%s\n'
 
1745
                          % (revision.rev.revision_id.decode('utf-8'),))
 
1746
        if not revision.rev.message:
 
1747
            to_file.write(indent + offset + '(no message)\n')
 
1748
        else:
 
1749
            message = revision.rev.message.rstrip('\r\n')
 
1750
            for l in message.split('\n'):
 
1751
                to_file.write(indent + offset + '%s\n' % (l,))
 
1752
 
 
1753
        if revision.delta is not None:
 
1754
            # Use the standard status output to display changes
 
1755
            from breezy.delta import report_delta
 
1756
            report_delta(to_file, revision.delta,
 
1757
                         short_status=self.delta_format == 1,
 
1758
                         show_ids=self.show_ids, indent=indent + offset)
 
1759
        if revision.diff is not None:
 
1760
            self.show_diff(self.to_exact_file, revision.diff, '      ')
 
1761
        to_file.write('\n')
 
1762
 
 
1763
 
 
1764
class LineLogFormatter(LogFormatter):
 
1765
 
 
1766
    supports_merge_revisions = True
 
1767
    preferred_levels = 1
 
1768
    supports_tags = True
 
1769
 
 
1770
    def __init__(self, *args, **kwargs):
 
1771
        super(LineLogFormatter, self).__init__(*args, **kwargs)
 
1772
        width = terminal_width()
 
1773
        if width is not None:
 
1774
            # we need one extra space for terminals that wrap on last char
 
1775
            width = width - 1
 
1776
        self._max_chars = width
 
1777
 
 
1778
    def truncate(self, str, max_len):
 
1779
        if max_len is None or len(str) <= max_len:
 
1780
            return str
 
1781
        return str[:max_len - 3] + '...'
 
1782
 
 
1783
    def date_string(self, rev):
 
1784
        return format_date(rev.timestamp, rev.timezone or 0,
 
1785
                           self.show_timezone, date_fmt="%Y-%m-%d",
 
1786
                           show_offset=False)
 
1787
 
 
1788
    def message(self, rev):
346
1789
        if not rev.message:
347
 
            print >>to_file,  '      (no message)'
348
 
        else:
349
 
            for l in rev.message.split('\n'):
350
 
                print >>to_file,  '      ' + l
351
 
 
352
 
        # TODO: Why not show the modified files in a shorter form as
353
 
        # well? rewrap them single lines of appropriate length
354
 
        if delta != None:
355
 
            delta.show(to_file, self.show_ids)
356
 
        print
357
 
 
358
 
 
359
 
 
360
 
FORMATTERS = {'long': LongLogFormatter,
361
 
              'short': ShortLogFormatter,
362
 
              }
 
1790
            return '(no message)'
 
1791
        else:
 
1792
            return rev.message
 
1793
 
 
1794
    def log_revision(self, revision):
 
1795
        indent = '  ' * revision.merge_depth
 
1796
        self.to_file.write(self.log_string(revision.revno, revision.rev,
 
1797
                                           self._max_chars, revision.tags, indent))
 
1798
        self.to_file.write('\n')
 
1799
 
 
1800
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
 
1801
        """Format log info into one string. Truncate tail of string
 
1802
 
 
1803
        :param revno:      revision number or None.
 
1804
                           Revision numbers counts from 1.
 
1805
        :param rev:        revision object
 
1806
        :param max_chars:  maximum length of resulting string
 
1807
        :param tags:       list of tags or None
 
1808
        :param prefix:     string to prefix each line
 
1809
        :return:           formatted truncated string
 
1810
        """
 
1811
        out = []
 
1812
        if revno:
 
1813
            # show revno only when is not None
 
1814
            out.append("%s:" % revno)
 
1815
        if max_chars is not None:
 
1816
            out.append(self.truncate(
 
1817
                self.short_author(rev), (max_chars + 3) // 4))
 
1818
        else:
 
1819
            out.append(self.short_author(rev))
 
1820
        out.append(self.date_string(rev))
 
1821
        if len(rev.parent_ids) > 1:
 
1822
            out.append('[merge]')
 
1823
        if tags:
 
1824
            tag_str = '{%s}' % (', '.join(sorted(tags)))
 
1825
            out.append(tag_str)
 
1826
        out.append(rev.get_summary())
 
1827
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
 
1828
 
 
1829
 
 
1830
class GnuChangelogLogFormatter(LogFormatter):
 
1831
 
 
1832
    supports_merge_revisions = True
 
1833
    supports_delta = True
 
1834
 
 
1835
    def log_revision(self, revision):
 
1836
        """Log a revision, either merged or not."""
 
1837
        to_file = self.to_file
 
1838
 
 
1839
        date_str = format_date(revision.rev.timestamp,
 
1840
                               revision.rev.timezone or 0,
 
1841
                               self.show_timezone,
 
1842
                               date_fmt='%Y-%m-%d',
 
1843
                               show_offset=False)
 
1844
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1845
        committer_str = committer_str.replace(' <', '  <')
 
1846
        to_file.write('%s  %s\n\n' % (date_str, committer_str))
 
1847
 
 
1848
        if revision.delta is not None and revision.delta.has_changed():
 
1849
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
 
1850
                if c.path[0] is None:
 
1851
                    path = c.path[1]
 
1852
                else:
 
1853
                    path = c.path[0]
 
1854
                to_file.write('\t* %s:\n' % (path,))
 
1855
            for c in revision.delta.renamed + revision.delta.copied:
 
1856
                # For renamed files, show both the old and the new path
 
1857
                to_file.write('\t* %s:\n\t* %s:\n' % (c.path[0], c.path[1]))
 
1858
            to_file.write('\n')
 
1859
 
 
1860
        if not revision.rev.message:
 
1861
            to_file.write('\tNo commit message\n')
 
1862
        else:
 
1863
            message = revision.rev.message.rstrip('\r\n')
 
1864
            for l in message.split('\n'):
 
1865
                to_file.write('\t%s\n' % (l.lstrip(),))
 
1866
            to_file.write('\n')
 
1867
 
 
1868
 
 
1869
def line_log(rev, max_chars):
 
1870
    lf = LineLogFormatter(None)
 
1871
    return lf.log_string(None, rev, max_chars)
 
1872
 
 
1873
 
 
1874
class LogFormatterRegistry(registry.Registry):
 
1875
    """Registry for log formatters"""
 
1876
 
 
1877
    def make_formatter(self, name, *args, **kwargs):
 
1878
        """Construct a formatter from arguments.
 
1879
 
 
1880
        :param name: Name of the formatter to construct.  'short', 'long' and
 
1881
            'line' are built-in.
 
1882
        """
 
1883
        return self.get(name)(*args, **kwargs)
 
1884
 
 
1885
    def get_default(self, branch):
 
1886
        c = branch.get_config_stack()
 
1887
        return self.get(c.get('log_format'))
 
1888
 
 
1889
 
 
1890
log_formatter_registry = LogFormatterRegistry()
 
1891
 
 
1892
 
 
1893
log_formatter_registry.register('short', ShortLogFormatter,
 
1894
                                'Moderately short log format.')
 
1895
log_formatter_registry.register('long', LongLogFormatter,
 
1896
                                'Detailed log format.')
 
1897
log_formatter_registry.register('line', LineLogFormatter,
 
1898
                                'Log format with one line per revision.')
 
1899
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
 
1900
                                'Format used by GNU ChangeLog files.')
 
1901
 
 
1902
 
 
1903
def register_formatter(name, formatter):
 
1904
    log_formatter_registry.register(name, formatter)
363
1905
 
364
1906
 
365
1907
def log_formatter(name, *args, **kwargs):
366
 
    from bzrlib.errors import BzrCommandError
367
 
    
368
 
    try:
369
 
        return FORMATTERS[name](*args, **kwargs)
 
1908
    """Construct a formatter from arguments.
 
1909
 
 
1910
    name -- Name of the formatter to construct; currently 'long', 'short' and
 
1911
        'line' are supported.
 
1912
    """
 
1913
    try:
 
1914
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
 
1915
    except KeyError:
 
1916
        raise errors.CommandError(
 
1917
            gettext("unknown log formatter: %r") % name)
 
1918
 
 
1919
 
 
1920
def author_list_all(rev):
 
1921
    return rev.get_apparent_authors()[:]
 
1922
 
 
1923
 
 
1924
def author_list_first(rev):
 
1925
    lst = rev.get_apparent_authors()
 
1926
    try:
 
1927
        return [lst[0]]
370
1928
    except IndexError:
371
 
        raise BzrCommandError("unknown log formatter: %r" % name)
372
 
 
373
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
374
 
    # deprecated; for compatability
375
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
376
 
    lf.show(revno, rev, delta)
 
1929
        return []
 
1930
 
 
1931
 
 
1932
def author_list_committer(rev):
 
1933
    return [rev.committer]
 
1934
 
 
1935
 
 
1936
author_list_registry = registry.Registry()
 
1937
 
 
1938
author_list_registry.register('all', author_list_all,
 
1939
                              'All authors')
 
1940
 
 
1941
author_list_registry.register('first', author_list_first,
 
1942
                              'The first author')
 
1943
 
 
1944
author_list_registry.register('committer', author_list_committer,
 
1945
                              'The committer')
 
1946
 
 
1947
 
 
1948
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
 
1949
                           log_format='long'):
 
1950
    """Show the change in revision history comparing the old revision history to the new one.
 
1951
 
 
1952
    :param branch: The branch where the revisions exist
 
1953
    :param old_rh: The old revision history
 
1954
    :param new_rh: The new revision history
 
1955
    :param to_file: A file to write the results to. If None, stdout will be used
 
1956
    """
 
1957
    if to_file is None:
 
1958
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
 
1959
                                                            errors='replace')
 
1960
    lf = log_formatter(log_format,
 
1961
                       show_ids=False,
 
1962
                       to_file=to_file,
 
1963
                       show_timezone='original')
 
1964
 
 
1965
    # This is the first index which is different between
 
1966
    # old and new
 
1967
    base_idx = None
 
1968
    for i in range(max(len(new_rh), len(old_rh))):
 
1969
        if (len(new_rh) <= i
 
1970
            or len(old_rh) <= i
 
1971
                or new_rh[i] != old_rh[i]):
 
1972
            base_idx = i
 
1973
            break
 
1974
 
 
1975
    if base_idx is None:
 
1976
        to_file.write('Nothing seems to have changed\n')
 
1977
        return
 
1978
    # TODO: It might be nice to do something like show_log
 
1979
    # and show the merged entries. But since this is the
 
1980
    # removed revisions, it shouldn't be as important
 
1981
    if base_idx < len(old_rh):
 
1982
        to_file.write('*' * 60)
 
1983
        to_file.write('\nRemoved Revisions:\n')
 
1984
        for i in range(base_idx, len(old_rh)):
 
1985
            rev = branch.repository.get_revision(old_rh[i])
 
1986
            lr = LogRevision(rev, i + 1, 0, None)
 
1987
            lf.log_revision(lr)
 
1988
        to_file.write('*' * 60)
 
1989
        to_file.write('\n\n')
 
1990
    if base_idx < len(new_rh):
 
1991
        to_file.write('Added Revisions:\n')
 
1992
        show_log(branch,
 
1993
                 lf,
 
1994
                 verbose=False,
 
1995
                 direction='forward',
 
1996
                 start_revision=base_idx + 1,
 
1997
                 end_revision=len(new_rh))
 
1998
 
 
1999
 
 
2000
def get_history_change(old_revision_id, new_revision_id, repository):
 
2001
    """Calculate the uncommon lefthand history between two revisions.
 
2002
 
 
2003
    :param old_revision_id: The original revision id.
 
2004
    :param new_revision_id: The new revision id.
 
2005
    :param repository: The repository to use for the calculation.
 
2006
 
 
2007
    return old_history, new_history
 
2008
    """
 
2009
    old_history = []
 
2010
    old_revisions = set()
 
2011
    new_history = []
 
2012
    new_revisions = set()
 
2013
    graph = repository.get_graph()
 
2014
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
2015
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
 
2016
    stop_revision = None
 
2017
    do_old = True
 
2018
    do_new = True
 
2019
    while do_new or do_old:
 
2020
        if do_new:
 
2021
            try:
 
2022
                new_revision = next(new_iter)
 
2023
            except StopIteration:
 
2024
                do_new = False
 
2025
            else:
 
2026
                new_history.append(new_revision)
 
2027
                new_revisions.add(new_revision)
 
2028
                if new_revision in old_revisions:
 
2029
                    stop_revision = new_revision
 
2030
                    break
 
2031
        if do_old:
 
2032
            try:
 
2033
                old_revision = next(old_iter)
 
2034
            except StopIteration:
 
2035
                do_old = False
 
2036
            else:
 
2037
                old_history.append(old_revision)
 
2038
                old_revisions.add(old_revision)
 
2039
                if old_revision in new_revisions:
 
2040
                    stop_revision = old_revision
 
2041
                    break
 
2042
    new_history.reverse()
 
2043
    old_history.reverse()
 
2044
    if stop_revision is not None:
 
2045
        new_history = new_history[new_history.index(stop_revision) + 1:]
 
2046
        old_history = old_history[old_history.index(stop_revision) + 1:]
 
2047
    return old_history, new_history
 
2048
 
 
2049
 
 
2050
def show_branch_change(branch, output, old_revno, old_revision_id):
 
2051
    """Show the changes made to a branch.
 
2052
 
 
2053
    :param branch: The branch to show changes about.
 
2054
    :param output: A file-like object to write changes to.
 
2055
    :param old_revno: The revno of the old tip.
 
2056
    :param old_revision_id: The revision_id of the old tip.
 
2057
    """
 
2058
    new_revno, new_revision_id = branch.last_revision_info()
 
2059
    old_history, new_history = get_history_change(old_revision_id,
 
2060
                                                  new_revision_id,
 
2061
                                                  branch.repository)
 
2062
    if old_history == [] and new_history == []:
 
2063
        output.write('Nothing seems to have changed\n')
 
2064
        return
 
2065
 
 
2066
    log_format = log_formatter_registry.get_default(branch)
 
2067
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
 
2068
    if old_history != []:
 
2069
        output.write('*' * 60)
 
2070
        output.write('\nRemoved Revisions:\n')
 
2071
        show_flat_log(branch.repository, old_history, old_revno, lf)
 
2072
        output.write('*' * 60)
 
2073
        output.write('\n\n')
 
2074
    if new_history != []:
 
2075
        output.write('Added Revisions:\n')
 
2076
        start_revno = new_revno - len(new_history) + 1
 
2077
        show_log(branch, lf, verbose=False, direction='forward',
 
2078
                 start_revision=start_revno)
 
2079
 
 
2080
 
 
2081
def show_flat_log(repository, history, last_revno, lf):
 
2082
    """Show a simple log of the specified history.
 
2083
 
 
2084
    :param repository: The repository to retrieve revisions from.
 
2085
    :param history: A list of revision_ids indicating the lefthand history.
 
2086
    :param last_revno: The revno of the last revision_id in the history.
 
2087
    :param lf: The log formatter to use.
 
2088
    """
 
2089
    revisions = repository.get_revisions(history)
 
2090
    for i, rev in enumerate(revisions):
 
2091
        lr = LogRevision(rev, i + last_revno, 0, None)
 
2092
        lf.log_revision(lr)
 
2093
 
 
2094
 
 
2095
def _get_info_for_log_files(revisionspec_list, file_list, exit_stack):
 
2096
    """Find files and kinds given a list of files and a revision range.
 
2097
 
 
2098
    We search for files at the end of the range. If not found there,
 
2099
    we try the start of the range.
 
2100
 
 
2101
    :param revisionspec_list: revision range as parsed on the command line
 
2102
    :param file_list: the list of paths given on the command line;
 
2103
      the first of these can be a branch location or a file path,
 
2104
      the remainder must be file paths
 
2105
    :param exit_stack: When the branch returned is read locked,
 
2106
      an unlock call will be queued to the exit stack.
 
2107
    :return: (branch, info_list, start_rev_info, end_rev_info) where
 
2108
      info_list is a list of (relative_path, found, kind) tuples where
 
2109
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
 
2110
      branch will be read-locked.
 
2111
    """
 
2112
    from breezy.builtins import _get_revision_range
 
2113
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2114
        file_list[0])
 
2115
    exit_stack.enter_context(b.lock_read())
 
2116
    # XXX: It's damn messy converting a list of paths to relative paths when
 
2117
    # those paths might be deleted ones, they might be on a case-insensitive
 
2118
    # filesystem and/or they might be in silly locations (like another branch).
 
2119
    # For example, what should "log bzr://branch/dir/file1 file2" do? (Is
 
2120
    # file2 implicitly in the same dir as file1 or should its directory be
 
2121
    # taken from the current tree somehow?) For now, this solves the common
 
2122
    # case of running log in a nested directory, assuming paths beyond the
 
2123
    # first one haven't been deleted ...
 
2124
    if tree:
 
2125
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
2126
    else:
 
2127
        relpaths = [path] + file_list[1:]
 
2128
    info_list = []
 
2129
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
 
2130
                                                       "log")
 
2131
    if relpaths in ([], [u'']):
 
2132
        return b, [], start_rev_info, end_rev_info
 
2133
    if start_rev_info is None and end_rev_info is None:
 
2134
        if tree is None:
 
2135
            tree = b.basis_tree()
 
2136
        tree1 = None
 
2137
        for fp in relpaths:
 
2138
            kind = _get_kind_for_file(tree, fp)
 
2139
            if not kind:
 
2140
                # go back to when time began
 
2141
                if tree1 is None:
 
2142
                    try:
 
2143
                        rev1 = b.get_rev_id(1)
 
2144
                    except errors.NoSuchRevision:
 
2145
                        # No history at all
 
2146
                        kind = None
 
2147
                    else:
 
2148
                        tree1 = b.repository.revision_tree(rev1)
 
2149
                if tree1:
 
2150
                    kind = _get_kind_for_file(tree1, fp)
 
2151
            info_list.append((fp, kind))
 
2152
 
 
2153
    elif start_rev_info == end_rev_info:
 
2154
        # One revision given - file must exist in it
 
2155
        tree = b.repository.revision_tree(end_rev_info.rev_id)
 
2156
        for fp in relpaths:
 
2157
            kind = _get_kind_for_file(tree, fp)
 
2158
            info_list.append((fp, kind))
 
2159
 
 
2160
    else:
 
2161
        # Revision range given. Get the file-id from the end tree.
 
2162
        # If that fails, try the start tree.
 
2163
        rev_id = end_rev_info.rev_id
 
2164
        if rev_id is None:
 
2165
            tree = b.basis_tree()
 
2166
        else:
 
2167
            tree = b.repository.revision_tree(rev_id)
 
2168
        tree1 = None
 
2169
        for fp in relpaths:
 
2170
            kind = _get_kind_for_file(tree, fp)
 
2171
            if not kind:
 
2172
                if tree1 is None:
 
2173
                    rev_id = start_rev_info.rev_id
 
2174
                    if rev_id is None:
 
2175
                        rev1 = b.get_rev_id(1)
 
2176
                        tree1 = b.repository.revision_tree(rev1)
 
2177
                    else:
 
2178
                        tree1 = b.repository.revision_tree(rev_id)
 
2179
                kind = _get_kind_for_file(tree1, fp)
 
2180
            info_list.append((fp, kind))
 
2181
    return b, info_list, start_rev_info, end_rev_info
 
2182
 
 
2183
 
 
2184
def _get_kind_for_file(tree, path):
 
2185
    """Return the kind of a path or None if it doesn't exist."""
 
2186
    with tree.lock_read():
 
2187
        try:
 
2188
            return tree.stored_kind(path)
 
2189
        except errors.NoSuchFile:
 
2190
            return None
 
2191
 
 
2192
 
 
2193
properties_handler_registry = registry.Registry()
 
2194
 
 
2195
# Use the properties handlers to print out bug information if available
 
2196
 
 
2197
 
 
2198
def _bugs_properties_handler(revision):
 
2199
    fixed_bug_urls = []
 
2200
    related_bug_urls = []
 
2201
    for bug_url, status in revision.iter_bugs():
 
2202
        if status == 'fixed':
 
2203
            fixed_bug_urls.append(bug_url)
 
2204
        elif status == 'related':
 
2205
            related_bug_urls.append(bug_url)
 
2206
    ret = {}
 
2207
    if fixed_bug_urls:
 
2208
        text = ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls))
 
2209
        ret[text] = ' '.join(fixed_bug_urls)
 
2210
    if related_bug_urls:
 
2211
        text = ngettext('related bug', 'related bugs',
 
2212
                        len(related_bug_urls))
 
2213
        ret[text] = ' '.join(related_bug_urls)
 
2214
    return ret
 
2215
 
 
2216
 
 
2217
properties_handler_registry.register('bugs_properties_handler',
 
2218
                                     _bugs_properties_handler)
 
2219
 
 
2220
 
 
2221
# adapters which revision ids to log are filtered. When log is called, the
 
2222
# log_rev_iterator is adapted through each of these factory methods.
 
2223
# Plugins are welcome to mutate this list in any way they like - as long
 
2224
# as the overall behaviour is preserved. At this point there is no extensible
 
2225
# mechanism for getting parameters to each factory method, and until there is
 
2226
# this won't be considered a stable api.
 
2227
log_adapters = [
 
2228
    # core log logic
 
2229
    _make_batch_filter,
 
2230
    # read revision objects
 
2231
    _make_revision_objects,
 
2232
    # filter on log messages
 
2233
    _make_search_filter,
 
2234
    # generate deltas for things we will show
 
2235
    _make_delta_filter
 
2236
    ]