/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 bzrlib/log.py

  • Committer: Ian Clatworthy
  • Date: 2009-03-25 05:33:40 UTC
  • mto: (4205.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 4206.
  • Revision ID: ian.clatworthy@canonical.com-20090325053340-86gbo7fh0dk09d32
get directory logging working again

Show diffs side-by-side

added added

removed removed

Lines of Context:
65
65
lazy_import(globals(), """
66
66
 
67
67
from bzrlib import (
 
68
    bzrdir,
68
69
    config,
69
70
    diff,
70
71
    errors,
140
141
    return rh
141
142
 
142
143
 
 
144
class LogRequest(object):
 
145
    """Query parameters for logging a branch."""
 
146
 
 
147
    def __init__(self,
 
148
        direction='reverse',
 
149
        specific_fileids=None,
 
150
        start_revision=None,
 
151
        end_revision=None,
 
152
        limit=None,
 
153
        message_search=None,
 
154
        levels=1,
 
155
        generate_tags=True,
 
156
        delta_type=None,
 
157
        diff_type=None,
 
158
        _match_using_deltas=True,
 
159
        ):
 
160
        """Create a logging request.
 
161
 
 
162
        Each of these parameter become a public attribute of the object.
 
163
 
 
164
        :param direction: 'reverse' (default) is latest to earliest;
 
165
          'forward' is earliest to latest.
 
166
 
 
167
        :param specific_fileids: If not None, only include revisions
 
168
          affecting the specified files, rather than all revisions.
 
169
 
 
170
        :param start_revision: If not None, only generate
 
171
          revisions >= start_revision
 
172
 
 
173
        :param end_revision: If not None, only generate
 
174
          revisions <= end_revision
 
175
 
 
176
        :param limit: If set, generate only 'limit' revisions, all revisions
 
177
          are shown if None or 0.
 
178
 
 
179
        :param message_search: If not None, only include revisions with
 
180
          matching commit messages
 
181
 
 
182
        :param levels: the number of levels of revisions to
 
183
          generate; 1 for just the mainline; 0 for all levels.
 
184
 
 
185
        :param generate_tags: If True, include tags for matched revisions.
 
186
 
 
187
        :param delta_type: Either 'full', 'partial' or None.
 
188
          'full' means generate the complete delta - adds/deletes/modifies/etc;
 
189
          'partial' means filter the delta using specific_fileids;
 
190
          None means do not generate any delta.
 
191
 
 
192
        :param diff_type: Either 'full', 'partial' or None.
 
193
          'full' means generate the complete diff - adds/deletes/modifies/etc;
 
194
          'partial' means filter the diff using specific_fileids;
 
195
          None means do not generate any diff.
 
196
 
 
197
        :param _match_using_deltas: a private parameter controlling the
 
198
          algorithm used for matching specific_fileids. This parameter
 
199
          may be removed in the future so bzrlib client code should NOT
 
200
          use it.
 
201
        """
 
202
        self.direction = direction
 
203
        self.specific_fileids = specific_fileids
 
204
        self.start_revision = start_revision
 
205
        self.end_revision = end_revision
 
206
        self.limit = limit
 
207
        self.message_search = message_search
 
208
        self.levels = levels
 
209
        self.generate_tags = generate_tags
 
210
        self.delta_type = delta_type
 
211
        self.diff_type = diff_type
 
212
        # Add 'private' attributes for features that may be deprecated
 
213
        self._match_using_deltas = _match_using_deltas
 
214
        self._allow_single_merge_revision = True
 
215
 
 
216
 
 
217
def show_log_request(branch, lf, rqst):
 
218
    """Write out human-readable log of commits to this branch.
 
219
 
 
220
    :param lf: The LogFormatter object showing the output.
 
221
 
 
222
    :param rqst: The LogRequest object specifying the query parameters.
 
223
    """
 
224
    branch.lock_read()
 
225
    try:
 
226
        if getattr(lf, 'begin_log', None):
 
227
            lf.begin_log()
 
228
 
 
229
        _show_log_request(branch, lf, rqst)
 
230
 
 
231
        if getattr(lf, 'end_log', None):
 
232
            lf.end_log()
 
233
    finally:
 
234
        branch.unlock()
 
235
 
 
236
 
143
237
def show_log(branch,
144
238
             lf,
145
239
             specific_fileid=None,
152
246
             show_diff=False):
153
247
    """Write out human-readable log of commits to this branch.
154
248
 
 
249
    Note: show_log_request() is now the preferred API to this one.
 
250
    This function is being retained for backwards compatibility but
 
251
    should not be extended with new parameters.
 
252
 
155
253
    :param lf: The LogFormatter object showing the output.
156
254
 
157
255
    :param specific_fileid: If not None, list only the commits affecting the
174
272
 
175
273
    :param show_diff: If True, output a diff after each revision.
176
274
    """
177
 
    branch.lock_read()
178
 
    try:
179
 
        if getattr(lf, 'begin_log', None):
180
 
            lf.begin_log()
181
 
 
182
 
        _show_log(branch, lf, specific_fileid, verbose, direction,
183
 
                  start_revision, end_revision, search, limit, show_diff)
184
 
 
185
 
        if getattr(lf, 'end_log', None):
186
 
            lf.end_log()
187
 
    finally:
188
 
        branch.unlock()
189
 
 
190
 
 
191
 
def _show_log(branch,
192
 
             lf,
193
 
             specific_fileid=None,
194
 
             verbose=False,
195
 
             direction='reverse',
196
 
             start_revision=None,
197
 
             end_revision=None,
198
 
             search=None,
199
 
             limit=None,
200
 
             show_diff=False):
201
 
    """Worker function for show_log - see show_log."""
 
275
    # Convert old-style parameters to new-style parameters
 
276
    if specific_fileid is not None:
 
277
        file_ids = [specific_fileid]
 
278
    else:
 
279
        file_ids = None
 
280
    if verbose:
 
281
        if file_ids:
 
282
            delta_type = 'partial'
 
283
        else:
 
284
            delta_type = 'full'
 
285
    else:
 
286
        delta_type = None
 
287
    if show_diff:
 
288
        if file_ids:
 
289
            diff_type = 'partial'
 
290
        else:
 
291
            diff_type = 'full'
 
292
    else:
 
293
        diff_type = None
 
294
 
 
295
    # Build the request and execute it
 
296
    rqst = LogRequest(direction=direction, specific_fileids=file_ids,
 
297
        start_revision=start_revision, end_revision=end_revision,
 
298
        limit=limit, message_search=search,
 
299
        delta_type=delta_type, diff_type=diff_type)
 
300
    show_log_request(branch, lf, rqst)
 
301
 
 
302
 
 
303
def _show_log_request(branch, lf, rqst):
 
304
    """Worker function for show_log_request - see show_log_request."""
202
305
    if not isinstance(lf, LogFormatter):
203
306
        warn("not a LogFormatter instance: %r" % lf)
204
 
    if specific_fileid:
205
 
        trace.mutter('get log for file_id %r', specific_fileid)
206
307
 
207
 
    # Consult the LogFormatter about what it needs and can handle
208
 
    levels_to_display = lf.get_levels()
209
 
    generate_merge_revisions = levels_to_display != 1
210
 
    allow_single_merge_revision = True
 
308
    # Tweak the LogRequest based on what the LogFormatter can handle.
 
309
    # (There's no point generating stuff if the formatter can't display it.)
 
310
    rqst.levels = lf.get_levels()
 
311
    if not getattr(lf, 'supports_tags', False):
 
312
        rqst.generate_tags = False
 
313
    if not getattr(lf, 'supports_delta', False):
 
314
        rqst.delta_type = None
 
315
    if not getattr(lf, 'supports_diff', False):
 
316
        rqst.diff_type = None
211
317
    if not getattr(lf, 'supports_merge_revisions', False):
212
 
        allow_single_merge_revision = getattr(lf,
 
318
        rqst._allow_single_merge_revision = getattr(lf,
213
319
            'supports_single_merge_revision', False)
214
 
    generate_tags = getattr(lf, 'supports_tags', False)
215
 
    if generate_tags and branch.supports_tags():
216
 
        rev_tag_dict = branch.tags.get_reverse_tag_dict()
217
 
    else:
218
 
        rev_tag_dict = {}
219
 
    generate_delta = verbose and getattr(lf, 'supports_delta', False)
220
 
    generate_diff = show_diff and getattr(lf, 'supports_diff', False)
221
320
 
222
321
    # Find and print the interesting revisions
223
 
    repo = branch.repository
224
 
    log_count = 0
225
 
    revision_iterator = _create_log_revision_iterator(branch,
226
 
        start_revision, end_revision, direction, specific_fileid, search,
227
 
        generate_merge_revisions, allow_single_merge_revision,
228
 
        generate_delta, limited_output=limit > 0)
229
 
    for revs in revision_iterator:
230
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
231
 
            # Note: 0 levels means show everything; merge_depth counts from 0
232
 
            if levels_to_display != 0 and merge_depth >= levels_to_display:
233
 
                continue
234
 
            if generate_diff:
235
 
                diff = _format_diff(repo, rev, rev_id, specific_fileid)
236
 
            else:
237
 
                diff = None
238
 
            lr = LogRevision(rev, revno, merge_depth, delta,
239
 
                             rev_tag_dict.get(rev_id), diff)
240
 
            lf.log_revision(lr)
241
 
            if limit:
242
 
                log_count += 1
243
 
                if log_count >= limit:
244
 
                    return
245
 
 
246
 
 
247
 
def _format_diff(repo, rev, rev_id, specific_fileid):
248
 
    if len(rev.parent_ids) == 0:
249
 
        ancestor_id = _mod_revision.NULL_REVISION
250
 
    else:
251
 
        ancestor_id = rev.parent_ids[0]
252
 
    tree_1 = repo.revision_tree(ancestor_id)
253
 
    tree_2 = repo.revision_tree(rev_id)
254
 
    if specific_fileid:
255
 
        specific_files = [tree_2.id2path(specific_fileid)]
256
 
    else:
257
 
        specific_files = None
258
 
    s = StringIO()
259
 
    diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
260
 
        new_label='')
261
 
    return s.getvalue()
 
322
    generator = _LogGenerator(branch, rqst)
 
323
    for lr in generator.iter_log_revisions():
 
324
        lf.log_revision(lr)
262
325
 
263
326
 
264
327
class _StartNotLinearAncestor(Exception):
265
328
    """Raised when a start revision is not found walking left-hand history."""
266
329
 
267
330
 
268
 
def _create_log_revision_iterator(branch, start_revision, end_revision,
269
 
    direction, specific_fileid, search, generate_merge_revisions,
270
 
    allow_single_merge_revision, generate_delta, limited_output=False):
271
 
    """Create a revision iterator for log.
272
 
 
273
 
    :param branch: The branch being logged.
274
 
    :param start_revision: If not None, only show revisions >= start_revision
275
 
    :param end_revision: If not None, only show revisions <= end_revision
276
 
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
277
 
        earliest to latest.
278
 
    :param specific_fileid: If not None, list only the commits affecting the
279
 
        specified file.
280
 
    :param search: If not None, only show revisions with matching commit
281
 
        messages.
282
 
    :param generate_merge_revisions: If False, show only mainline revisions.
283
 
    :param allow_single_merge_revision: If True, logging of a single
284
 
        revision off the mainline is to be allowed
285
 
    :param generate_delta: Whether to generate a delta for each revision.
286
 
    :param limited_output: if True, the user only wants a limited result
287
 
 
288
 
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
289
 
        delta).
290
 
    """
291
 
    start_rev_id, end_rev_id = _get_revision_limits(branch, start_revision,
292
 
        end_revision)
293
 
 
294
 
    # Decide how file-ids are matched: delta-filtering vs per-file graph.
295
 
    # Delta filtering allows revisions to be displayed incrementally
296
 
    # though the total time is much slower for huge repositories: log -v
297
 
    # is the *lower* performance bound. At least until the split
298
 
    # inventory format arrives, per-file-graph needs to remain the
299
 
    # default except in verbose mode. Delta filtering should give more
300
 
    # accurate results (e.g. inclusion of FILE deletions) so arguably
301
 
    # it should always be used in the future.
302
 
    use_deltas_for_matching = specific_fileid and generate_delta
303
 
    delayed_graph_generation = not specific_fileid and (
304
 
            start_rev_id or end_rev_id or limited_output)
305
 
    generate_merges = generate_merge_revisions or (specific_fileid and
306
 
        not use_deltas_for_matching)
307
 
    view_revisions = _calc_view_revisions(branch, start_rev_id, end_rev_id,
308
 
        direction, generate_merges, allow_single_merge_revision,
309
 
        delayed_graph_generation=delayed_graph_generation)
310
 
    search_deltas_for_fileids = None
311
 
    if use_deltas_for_matching:
312
 
        search_deltas_for_fileids = set([specific_fileid])
313
 
    elif specific_fileid:
 
331
class _LogGenerator(object):
 
332
    """A generator of log revisions given a branch and a LogRequest."""
 
333
 
 
334
    def __init__(self, branch, rqst):
 
335
        self.branch = branch
 
336
        self.rqst = rqst
 
337
        if rqst.generate_tags and branch.supports_tags():
 
338
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
 
339
        else:
 
340
            self.rev_tag_dict = {}
 
341
 
 
342
    def iter_log_revisions(self):
 
343
        """Iterate over LogRevision objects.
 
344
 
 
345
        :return: An iterator yielding LogRevision objects.
 
346
        """
 
347
        rqst = self.rqst
 
348
        log_count = 0
 
349
        revision_iterator = self._create_log_revision_iterator()
 
350
        for revs in revision_iterator:
 
351
            for (rev_id, revno, merge_depth), rev, delta in revs:
 
352
                # 0 levels means show everything; merge_depth counts from 0
 
353
                if rqst.levels != 0 and merge_depth >= rqst.levels:
 
354
                    continue
 
355
                diff = self._format_diff(rev, rev_id)
 
356
                yield LogRevision(rev, revno, merge_depth, delta,
 
357
                    self.rev_tag_dict.get(rev_id), diff)
 
358
                if rqst.limit:
 
359
                    log_count += 1
 
360
                    if log_count >= rqst.limit:
 
361
                        return
 
362
 
 
363
    def _format_diff(self, rev, rev_id):
 
364
        diff_type = self.rqst.diff_type
 
365
        if diff_type is None:
 
366
            return None
 
367
        repo = self.branch.repository
 
368
        if len(rev.parent_ids) == 0:
 
369
            ancestor_id = _mod_revision.NULL_REVISION
 
370
        else:
 
371
            ancestor_id = rev.parent_ids[0]
 
372
        tree_1 = repo.revision_tree(ancestor_id)
 
373
        tree_2 = repo.revision_tree(rev_id)
 
374
        file_ids = self.rqst.specific_fileids
 
375
        if diff_type == 'partial' and file_ids is not None:
 
376
            specific_files = [tree_2.id2path(id) for id in file_ids]
 
377
        else:
 
378
            specific_files = None
 
379
        s = StringIO()
 
380
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
381
            new_label='')
 
382
        return s.getvalue()
 
383
 
 
384
    def _create_log_revision_iterator(self):
 
385
        """Create a revision iterator for log.
 
386
 
 
387
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
388
            delta).
 
389
        """
 
390
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
 
391
            self.branch, self.rqst.start_revision, self.rqst.end_revision)
 
392
        if self.rqst._match_using_deltas:
 
393
            return self._log_revision_iterator_using_delta_matching()
 
394
        else:
 
395
            # We're using the per-file-graph algorithm. This scales really
 
396
            # well but only makes sense if there is a single file and it's
 
397
            # not a directory
 
398
            file_count = len(self.rqst.specific_fileids)
 
399
            if file_count != 1:
 
400
                raise BzrError("illegal LogRequest: must match-using-deltas "
 
401
                    "when logging %d files" % file_count)
 
402
            return self._log_revision_iterator_using_per_file_graph()
 
403
 
 
404
    def _log_revision_iterator_using_delta_matching(self):
 
405
        # Get the base revisions, filtering by the revision range
 
406
        rqst = self.rqst
 
407
        generate_merge_revisions = rqst.levels != 1
 
408
        delayed_graph_generation = not rqst.specific_fileids and (
 
409
                rqst.limit or self.start_rev_id or self.end_rev_id)
 
410
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
411
            self.end_rev_id, rqst.direction, generate_merge_revisions,
 
412
            rqst._allow_single_merge_revision,
 
413
            delayed_graph_generation=delayed_graph_generation)
 
414
 
 
415
        # Apply the other filters
 
416
        return make_log_rev_iterator(self.branch, view_revisions,
 
417
            rqst.delta_type, rqst.message_search,
 
418
            file_ids=rqst.specific_fileids, direction=rqst.direction)
 
419
 
 
420
    def _log_revision_iterator_using_per_file_graph(self):
 
421
        # Get the base revisions, filtering by the revision range.
 
422
        # Note that we always generate the merge revisions because
 
423
        # filter_revisions_touching_file_id() requires them ...
 
424
        rqst = self.rqst
 
425
        view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
 
426
            self.end_rev_id, rqst.direction, True,
 
427
            rqst._allow_single_merge_revision)
314
428
        if not isinstance(view_revisions, list):
315
429
            view_revisions = list(view_revisions)
316
 
        view_revisions = _filter_revisions_touching_file_id(branch,
317
 
            specific_fileid, view_revisions,
318
 
            include_merges=generate_merge_revisions)
319
 
    return make_log_rev_iterator(branch, view_revisions, generate_delta,
320
 
        search, file_ids=search_deltas_for_fileids, direction=direction)
 
430
        view_revisions = _filter_revisions_touching_file_id(self.branch,
 
431
            rqst.specific_fileids[0], view_revisions,
 
432
            include_merges=rqst.levels != 1)
 
433
        return make_log_rev_iterator(self.branch, view_revisions,
 
434
            rqst.delta_type, rqst.message_search)
321
435
 
322
436
 
323
437
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
336
450
    generate_single_revision = (end_rev_id and start_rev_id == end_rev_id and
337
451
        (not generate_merge_revisions or not _has_merges(branch, end_rev_id)))
338
452
    if generate_single_revision:
339
 
        if end_rev_id == br_rev_id:
340
 
            # It's the tip
341
 
            return [(br_rev_id, br_revno, 0)]
342
 
        else:
343
 
            revno = branch.revision_id_to_dotted_revno(end_rev_id)
344
 
            if len(revno) > 1 and not allow_single_merge_revision:
345
 
                # It's a merge revision and the log formatter is
346
 
                # completely brain dead. This "feature" of allowing
347
 
                # log formatters incapable of displaying dotted revnos
348
 
                # ought to be deprecated IMNSHO. IGC 20091022
349
 
                raise errors.BzrCommandError('Selected log formatter only'
350
 
                    ' supports mainline revisions.')
351
 
            revno_str = '.'.join(str(n) for n in revno)
352
 
            return [(end_rev_id, revno_str, 0)]
 
453
        return _generate_one_revision(branch, end_rev_id, br_rev_id, br_revno,
 
454
            allow_single_merge_revision)
353
455
 
354
456
    # If we only want to see linear revisions, we can iterate ...
355
457
    if not generate_merge_revisions:
356
 
        result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
357
 
        # If a start limit was given and it's not obviously an
358
 
        # ancestor of the end limit, check it before outputting anything
359
 
        if direction == 'forward' or (start_rev_id
360
 
            and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
361
 
            try:
362
 
                result = list(result)
363
 
            except _StartNotLinearAncestor:
364
 
                raise errors.BzrCommandError('Start revision not found in'
365
 
                    ' left-hand history of end revision.')
366
 
        if direction == 'forward':
367
 
            result = reversed(list(result))
368
 
        return result
369
 
 
 
458
        return _generate_flat_revisions(branch, start_rev_id, end_rev_id,
 
459
            direction)
 
460
    else:
 
461
        return _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
462
            direction, delayed_graph_generation)
 
463
 
 
464
 
 
465
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno,
 
466
    allow_single_merge_revision):
 
467
    if rev_id == br_rev_id:
 
468
        # It's the tip
 
469
        return [(br_rev_id, br_revno, 0)]
 
470
    else:
 
471
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
472
        if len(revno) > 1 and not allow_single_merge_revision:
 
473
            # It's a merge revision and the log formatter is
 
474
            # completely brain dead. This "feature" of allowing
 
475
            # log formatters incapable of displaying dotted revnos
 
476
            # ought to be deprecated IMNSHO. IGC 20091022
 
477
            raise errors.BzrCommandError('Selected log formatter only'
 
478
                ' supports mainline revisions.')
 
479
        revno_str = '.'.join(str(n) for n in revno)
 
480
        return [(rev_id, revno_str, 0)]
 
481
 
 
482
 
 
483
def _generate_flat_revisions(branch, start_rev_id, end_rev_id, direction):
 
484
    result = _linear_view_revisions(branch, start_rev_id, end_rev_id)
 
485
    # If a start limit was given and it's not obviously an
 
486
    # ancestor of the end limit, check it before outputting anything
 
487
    if direction == 'forward' or (start_rev_id
 
488
        and not _is_obvious_ancestor(branch, start_rev_id, end_rev_id)):
 
489
        try:
 
490
            result = list(result)
 
491
        except _StartNotLinearAncestor:
 
492
            raise errors.BzrCommandError('Start revision not found in'
 
493
                ' left-hand history of end revision.')
 
494
    if direction == 'forward':
 
495
        result = reversed(result)
 
496
    return result
 
497
 
 
498
 
 
499
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
 
500
    delayed_graph_generation):
370
501
    # On large trees, generating the merge graph can take 30-60 seconds
371
502
    # so we delay doing it until a merge is detected, incrementally
372
503
    # returning initial (non-merge) revisions while we can.
544
675
    :param branch: The branch being logged.
545
676
    :param view_revisions: The revisions being viewed.
546
677
    :param generate_delta: Whether to generate a delta for each revision.
 
678
      Permitted values are None, 'full' and 'partial'.
547
679
    :param search: A user text search string.
548
680
    :param file_ids: If non empty, only revisions matching one or more of
549
681
      the file-ids are to be kept.
608
740
 
609
741
    :param branch: The branch being logged.
610
742
    :param generate_delta: Whether to generate a delta for each revision.
 
743
      Permitted values are None, 'full' and 'partial'.
611
744
    :param search: A user text search string.
612
745
    :param log_rev_iterator: An input iterator containing all revisions that
613
746
        could be displayed, in lists.
623
756
        generate_delta, fileids, direction)
624
757
 
625
758
 
626
 
def _generate_deltas(repository, log_rev_iterator, always_delta, fileids,
 
759
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
627
760
    direction):
628
761
    """Create deltas for each batch of revisions in log_rev_iterator.
629
762
 
647
780
        if check_fileids and not fileid_set:
648
781
            return
649
782
        revisions = [rev[1] for rev in revs]
650
 
        deltas = repository.get_deltas_for_revisions(revisions)
651
783
        new_revs = []
652
 
        for rev, delta in izip(revs, deltas):
653
 
            if check_fileids:
654
 
                if not _delta_matches_fileids(delta, fileid_set, stop_on):
655
 
                    continue
656
 
                elif not always_delta:
657
 
                    # Delta was created just for matching - ditch it
658
 
                    # Note: It would probably be a better UI to return
659
 
                    # a delta filtered by the file-ids, rather than
660
 
                    # None at all. That functional enhancement can
661
 
                    # come later ...
662
 
                    delta = None
663
 
            new_revs.append((rev[0], rev[1], delta))
 
784
        if delta_type == 'full' and not check_fileids:
 
785
            deltas = repository.get_deltas_for_revisions(revisions)
 
786
            for rev, delta in izip(revs, deltas):
 
787
                new_revs.append((rev[0], rev[1], delta))
 
788
        else:
 
789
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
 
790
            for rev, delta in izip(revs, deltas):
 
791
                if check_fileids:
 
792
                    if delta is None or not delta.has_changed():
 
793
                        continue
 
794
                    else:
 
795
                        _update_fileids(delta, fileid_set, stop_on)
 
796
                        if delta_type is None:
 
797
                            delta = None
 
798
                        elif delta_type == 'full':
 
799
                            # If the file matches all the time, rebuilding
 
800
                            # a full delta like this in addition to a partial
 
801
                            # one could be slow. However, it's likely the
 
802
                            # most revisions won't get this far, making it
 
803
                            # faster to filter on the partial deltas and
 
804
                            # build the occasional full delta than always
 
805
                            # building full deltas and filtering those.
 
806
                            rev_id = rev[0][0]
 
807
                            delta = repository.get_revision_delta(rev_id)
 
808
                new_revs.append((rev[0], rev[1], delta))
664
809
        yield new_revs
665
810
 
666
811
 
667
 
def _delta_matches_fileids(delta, fileids, stop_on='add'):
668
 
    """Check is a delta matches one of more file-ids.
669
 
 
670
 
    :param fileids: a set of fileids to match against.
 
812
def _update_fileids(delta, fileids, stop_on):
 
813
    """Update the set of file-ids to search based on file lifecycle events.
 
814
    
 
815
    :param fileids: a set of fileids to update
671
816
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
672
817
      fileids set once their add or remove entry is detected respectively
673
818
    """
674
 
    if not fileids:
675
 
        return False
676
 
    result = False
677
 
    for item in delta.added:
678
 
        if item[1] in fileids:
679
 
            if stop_on == 'add':
680
 
                fileids.remove(item[1])
681
 
            result = True
682
 
    for item in delta.removed:
683
 
        if item[1] in fileids:
684
 
            if stop_on == 'delete':
685
 
                fileids.remove(item[1])
686
 
            result = True
687
 
    if result:
688
 
        return True
689
 
    for l in (delta.modified, delta.renamed, delta.kind_changed):
690
 
        for item in l:
691
 
            if item[1] in fileids:
692
 
                return True
693
 
    return False
 
819
    if stop_on == 'add':
 
820
        for item in delta.added:
 
821
            if item[1] in fileids:
 
822
                fileids.remove(item[1])
 
823
    elif stop_on == 'delete':
 
824
        for item in delta.removed:
 
825
            if item[1] in fileids:
 
826
                fileids.remove(item[1])
694
827
 
695
828
 
696
829
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1617
1750
        lf.log_revision(lr)
1618
1751
 
1619
1752
 
1620
 
def _get_fileid_to_log(revision, tree, b, fp):
1621
 
    """Find the file-id to log for a file path in a revision range.
1622
 
 
1623
 
    :param revision: the revision range as parsed on the command line
1624
 
    :param tree: the working tree, if any
1625
 
    :param b: the branch
1626
 
    :param fp: file path
 
1753
def _get_info_for_log_files(revisionspec_list, file_list):
 
1754
    """Find file-ids and kinds given a list of files and a revision range.
 
1755
 
 
1756
    We search for files at the end of the range. If not found there,
 
1757
    we try the start of the range.
 
1758
 
 
1759
    :param revisionspec_list: revision range as parsed on the command line
 
1760
    :param file_list: the list of paths given on the command line;
 
1761
      the first of these can be a branch location or a file path,
 
1762
      the remainder must be file paths
 
1763
    :return: (branch, info_list, start_rev_info, end_rev_info) where
 
1764
      info_list is a list of (relative_path, file_id, kind) tuples where
 
1765
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
1627
1766
    """
1628
 
    if revision is None:
 
1767
    from builtins import _get_revision_range, safe_relpath_files
 
1768
    tree, b, path = bzrdir.BzrDir.open_containing_tree_or_branch(file_list[0])
 
1769
    # XXX: It's damn messy converting a list of paths to relative paths when
 
1770
    # those paths might be deleted ones, they might be on a case-insensitive
 
1771
    # filesystem and/or they might be in silly locations (like another branch).
 
1772
    # For example, what should "log bzr://branch/dir/file1 file2" do? (Is
 
1773
    # file2 implicitly in the same dir as file1 or should its directory be
 
1774
    # taken from the current tree somehow?) For now, this solves the common
 
1775
    # case of running log in a nested directory, assuming paths beyond the
 
1776
    # first one haven't been deleted ...
 
1777
    if tree:
 
1778
        relpaths = [path] + safe_relpath_files(tree, file_list[1:])
 
1779
    else:
 
1780
        relpaths = [path] + file_list[1:]
 
1781
    info_list = []
 
1782
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
 
1783
        "log")
 
1784
    if start_rev_info is None and end_rev_info is None:
1629
1785
        if tree is None:
1630
1786
            tree = b.basis_tree()
1631
 
        file_id = tree.path2id(fp)
1632
 
        if file_id is None:
1633
 
            # go back to when time began
1634
 
            try:
1635
 
                rev1 = b.get_rev_id(1)
1636
 
            except errors.NoSuchRevision:
1637
 
                # No history at all
1638
 
                file_id = None
1639
 
            else:
1640
 
                tree = b.repository.revision_tree(rev1)
1641
 
                file_id = tree.path2id(fp)
 
1787
        tree1 = None
 
1788
        for fp in relpaths:
 
1789
            file_id = tree.path2id(fp)
 
1790
            kind = _get_kind_for_file_id(tree, file_id)
 
1791
            if file_id is None:
 
1792
                # go back to when time began
 
1793
                if tree1 is None:
 
1794
                    try:
 
1795
                        rev1 = b.get_rev_id(1)
 
1796
                    except errors.NoSuchRevision:
 
1797
                        # No history at all
 
1798
                        file_id = None
 
1799
                        kind = None
 
1800
                    else:
 
1801
                        tree1 = b.repository.revision_tree(rev1)
 
1802
                if tree1:
 
1803
                    file_id = tree1.path2id(fp)
 
1804
                    kind = _get_kind_for_file_id(tree1, file_id)
 
1805
            info_list.append((fp, file_id, kind))
1642
1806
 
1643
 
    elif len(revision) == 1:
 
1807
    elif start_rev_info == end_rev_info:
1644
1808
        # One revision given - file must exist in it
1645
 
        tree = revision[0].as_tree(b)
1646
 
        file_id = tree.path2id(fp)
 
1809
        tree = b.repository.revision_tree(end_rev_info.rev_id)
 
1810
        for fp in relpaths:
 
1811
            file_id = tree.path2id(fp)
 
1812
            kind = _get_kind_for_file_id(tree, file_id)
 
1813
            info_list.append((fp, file_id, kind))
1647
1814
 
1648
 
    elif len(revision) == 2:
 
1815
    else:
1649
1816
        # Revision range given. Get the file-id from the end tree.
1650
1817
        # If that fails, try the start tree.
1651
 
        rev_id = revision[1].as_revision_id(b)
 
1818
        rev_id = end_rev_info.rev_id
1652
1819
        if rev_id is None:
1653
1820
            tree = b.basis_tree()
1654
1821
        else:
1655
 
            tree = revision[1].as_tree(b)
1656
 
        file_id = tree.path2id(fp)
1657
 
        if file_id is None:
1658
 
            rev_id = revision[0].as_revision_id(b)
1659
 
            if rev_id is None:
1660
 
                rev1 = b.get_rev_id(1)
1661
 
                tree = b.repository.revision_tree(rev1)
1662
 
            else:
1663
 
                tree = revision[0].as_tree(b)
 
1822
            tree = b.repository.revision_tree(rev_id)
 
1823
        tree1 = None
 
1824
        for fp in relpaths:
1664
1825
            file_id = tree.path2id(fp)
 
1826
            kind = _get_kind_for_file_id(tree, file_id)
 
1827
            if file_id is None:
 
1828
                if tree1 is None:
 
1829
                    rev_id = start_rev_info.rev_id
 
1830
                    if rev_id is None:
 
1831
                        rev1 = b.get_rev_id(1)
 
1832
                        tree1 = b.repository.revision_tree(rev1)
 
1833
                    else:
 
1834
                        tree1 = b.repository.revision_tree(rev_id)
 
1835
                file_id = tree1.path2id(fp)
 
1836
                kind = _get_kind_for_file_id(tree1, file_id)
 
1837
            info_list.append((fp, file_id, kind))
 
1838
    return b, info_list, start_rev_info, end_rev_info
 
1839
 
 
1840
 
 
1841
def _get_kind_for_file_id(tree, file_id):
 
1842
    """Return the kind of a file-id or None if it doesn't exist."""
 
1843
    if file_id is not None:
 
1844
        return tree.kind(file_id)
1665
1845
    else:
1666
 
        raise errors.BzrCommandError(
1667
 
            'bzr log --revision takes one or two values.')
1668
 
    return file_id
 
1846
        return None
1669
1847
 
1670
1848
 
1671
1849
properties_handler_registry = registry.Registry()