102
101
TODO: Perhaps some way to limit this to only particular revisions,
103
102
or to traverse a non-mainline set of revisions?
108
graph = branch.repository.get_graph()
109
history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
110
[_mod_revision.NULL_REVISION]))
111
for revision_id in reversed(history):
112
this_inv = branch.repository.get_inventory(revision_id)
113
if this_inv.has_id(file_id):
114
this_ie = this_inv[file_id]
115
this_path = this_inv.id2path(file_id)
117
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, []))
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)
119
113
# now we know how it was last time, and how it is in this revision.
120
114
# are those two states effectively the same or not?
122
if not this_ie and not last_ie:
123
# not present in either
125
elif this_ie and not last_ie:
126
yield revno, revision_id, "added " + this_path
127
elif not this_ie and last_ie:
129
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
130
120
elif this_path != last_path:
131
yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
132
elif (this_ie.text_size != last_ie.text_size
133
or this_ie.text_sha1 != last_ie.text_sha1):
134
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)
124
this_verifier = this_tree.get_file_verifier(this_path)
125
if (this_verifier != last_verifier):
126
yield revno, revision_id, "modified " + this_path
128
last_verifier = this_verifier
137
129
last_path = this_path
130
last_tree = this_tree
131
if last_path is None:
141
136
def show_log(branch,
143
specific_fileid=None,
145
139
direction='reverse',
146
140
start_revision=None,
147
141
end_revision=None,
181
168
:param match: Dictionary of search lists to use when matching revision
184
# Convert old-style parameters to new-style parameters
185
if specific_fileid is not None:
186
file_ids = [specific_fileid]
191
delta_type = 'partial'
195
174
delta_type = None
198
diff_type = 'partial'
180
if isinstance(start_revision, int):
182
start_revision = revisionspec.RevisionInfo(branch, start_revision)
183
except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
184
raise errors.InvalidRevisionNumber(start_revision)
186
if isinstance(end_revision, int):
188
end_revision = revisionspec.RevisionInfo(branch, end_revision)
189
except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
190
raise errors.InvalidRevisionNumber(end_revision)
192
if end_revision is not None and end_revision.revno == 0:
193
raise errors.InvalidRevisionNumber(end_revision.revno)
204
195
# Build the request and execute it
205
rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
196
rqst = make_log_request_dict(
206
198
start_revision=start_revision, end_revision=end_revision,
207
limit=limit, message_search=search,
208
delta_type=delta_type, diff_type=diff_type)
199
limit=limit, delta_type=delta_type, diff_type=diff_type)
209
200
Logger(branch, rqst).show(lf)
418
410
Subclasses may wish to override this.
420
return _DefaultLogGenerator(branch, rqst)
412
return _DefaultLogGenerator(branch, **rqst)
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)
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,
444
generate_merge_revisions=generate_merge_revisions,
445
delayed_graph_generation=delayed_graph_generation,
446
exclude_common_ancestry=exclude_common_ancestry)
448
# Apply the other filters
449
return make_log_rev_iterator(branch, view_revisions,
451
files=specific_files,
455
def _format_diff(branch, rev, diff_type, files=None):
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)
463
repo = branch.repository
464
if len(rev.parent_ids) == 0:
465
ancestor_id = _mod_revision.NULL_REVISION
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
473
specific_files = None
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)
423
481
class _StartNotLinearAncestor(Exception):
427
485
class _DefaultLogGenerator(LogGenerator):
428
486
"""The default generator of log revisions."""
430
def __init__(self, branch, rqst):
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,
431
495
self.branch = branch
433
if rqst.get('generate_tags') and branch.supports_tags():
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
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():
434
510
self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
436
512
self.rev_tag_dict = {}
441
517
:return: An iterator yielding LogRevision objects.
444
levels = rqst.get('levels')
445
limit = rqst.get('limit')
446
diff_type = rqst.get('diff_type')
447
show_signature = rqst.get('signature')
448
omit_merges = rqst.get('omit_merges')
450
520
revision_iterator = self._create_log_revision_iterator()
451
521
for revs in revision_iterator:
452
522
for (rev_id, revno, merge_depth), rev, delta in revs:
453
523
# 0 levels means show everything; merge_depth counts from 0
454
if levels != 0 and merge_depth >= levels:
456
if omit_merges and len(rev.parent_ids) > 1:
458
if diff_type is None:
524
if (self.levels != 0 and merge_depth is not None and
525
merge_depth >= self.levels):
527
if self.omit_merges and len(rev.parent_ids) > 1:
530
raise errors.GhostRevisionUnusableHere(rev_id)
531
if self.diff_type is None:
461
diff = self._format_diff(rev, rev_id, diff_type)
463
signature = format_signature_validity(rev_id,
464
self.branch.repository)
535
self.branch, rev, self.diff_type,
537
if self.show_signature:
538
signature = format_signature_validity(rev_id, self.branch)
467
yield LogRevision(rev, revno, merge_depth, delta,
542
rev, revno, merge_depth, delta,
468
543
self.rev_tag_dict.get(rev_id), diff, signature)
471
if log_count >= limit:
546
if log_count >= self.limit:
474
def _format_diff(self, rev, rev_id, diff_type):
475
repo = self.branch.repository
476
if len(rev.parent_ids) == 0:
477
ancestor_id = _mod_revision.NULL_REVISION
479
ancestor_id = rev.parent_ids[0]
480
tree_1 = repo.revision_tree(ancestor_id)
481
tree_2 = repo.revision_tree(rev_id)
482
file_ids = self.rqst.get('specific_fileids')
483
if diff_type == 'partial' and file_ids is not None:
484
specific_files = [tree_2.id2path(id) for id in file_ids]
486
specific_files = None
488
path_encoding = get_diff_header_encoding()
489
diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
490
new_label='', path_encoding=path_encoding)
493
549
def _create_log_revision_iterator(self):
494
550
"""Create a revision iterator for log.
496
552
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
499
self.start_rev_id, self.end_rev_id = _get_revision_limits(
500
self.branch, self.rqst.get('start_revision'),
501
self.rqst.get('end_revision'))
502
if self.rqst.get('_match_using_deltas'):
503
return self._log_revision_iterator_using_delta_matching()
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(
560
delta_type=self.delta_type,
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,
505
569
# We're using the per-file-graph algorithm. This scales really
506
570
# well but only makes sense if there is a single file and it's
507
571
# not a directory
508
file_count = len(self.rqst.get('specific_fileids'))
572
file_count = len(self.specific_files)
509
573
if file_count != 1:
510
raise BzrError("illegal LogRequest: must match-using-deltas "
574
raise errors.BzrError(
575
"illegal LogRequest: must match-using-deltas "
511
576
"when logging %d files" % file_count)
512
return self._log_revision_iterator_using_per_file_graph()
514
def _log_revision_iterator_using_delta_matching(self):
515
# Get the base revisions, filtering by the revision range
517
generate_merge_revisions = rqst.get('levels') != 1
518
delayed_graph_generation = not rqst.get('specific_fileids') and (
519
rqst.get('limit') or self.start_rev_id or self.end_rev_id)
520
view_revisions = _calc_view_revisions(
521
self.branch, self.start_rev_id, self.end_rev_id,
522
rqst.get('direction'),
523
generate_merge_revisions=generate_merge_revisions,
524
delayed_graph_generation=delayed_graph_generation,
525
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
527
# Apply the other filters
528
return make_log_rev_iterator(self.branch, view_revisions,
529
rqst.get('delta_type'), rqst.get('match'),
530
file_ids=rqst.get('specific_fileids'),
531
direction=rqst.get('direction'))
533
def _log_revision_iterator_using_per_file_graph(self):
534
# Get the base revisions, filtering by the revision range.
535
# Note that we always generate the merge revisions because
536
# filter_revisions_touching_file_id() requires them ...
538
view_revisions = _calc_view_revisions(
539
self.branch, self.start_rev_id, self.end_rev_id,
540
rqst.get('direction'), generate_merge_revisions=True,
541
exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
542
if not isinstance(view_revisions, list):
543
view_revisions = list(view_revisions)
544
view_revisions = _filter_revisions_touching_file_id(self.branch,
545
rqst.get('specific_fileids')[0], view_revisions,
546
include_merges=rqst.get('levels') != 1)
547
return make_log_rev_iterator(self.branch, view_revisions,
548
rqst.get('delta_type'), rqst.get('match'))
577
return _log_revision_iterator_using_per_file_graph(
579
delta_type=self.delta_type,
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
551
589
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
722
761
:param exclude_common_ancestry: Whether the start_rev_id should be part of
723
762
the iterated revisions.
724
763
:return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
764
dotted_revno will be None for ghosts
725
765
:raises _StartNotLinearAncestor: if a start_rev_id is specified but
726
766
is not found walking the left-hand history
728
br_revno, br_rev_id = branch.last_revision_info()
729
768
repo = branch.repository
730
769
graph = repo.get_graph()
731
770
if start_rev_id is None and end_rev_id is None:
733
for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
734
(_mod_revision.NULL_REVISION,)):
735
yield revision_id, str(cur_revno), 0
771
if branch._format.stores_revno() or \
772
config.GlobalStack().get('calculate_revnos'):
774
br_revno, br_rev_id = branch.last_revision_info()
775
except errors.GhostRevisionsHaveNoRevno:
776
br_rev_id = branch.last_revision()
781
br_rev_id = branch.last_revision()
784
graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
785
(_mod_revision.NULL_REVISION,))
788
revision_id = next(graph_iter)
789
except errors.RevisionNotPresent as e:
791
yield e.revision_id, None, None
793
except StopIteration:
796
yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
797
if cur_revno is not None:
800
br_rev_id = branch.last_revision()
738
801
if end_rev_id is None:
739
802
end_rev_id = br_rev_id
740
803
found_start = start_rev_id is None
741
for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
742
(_mod_revision.NULL_REVISION,)):
743
revno_str = _compute_revno_str(branch, revision_id)
744
if not found_start and revision_id == start_rev_id:
745
if not exclude_common_ancestry:
804
graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
805
(_mod_revision.NULL_REVISION,))
808
revision_id = next(graph_iter)
809
except StopIteration:
811
except errors.RevisionNotPresent as e:
813
yield e.revision_id, None, None
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
746
823
yield revision_id, revno_str, 0
750
yield revision_id, revno_str, 0
753
raise _StartNotLinearAncestor()
825
raise _StartNotLinearAncestor()
756
828
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
816
889
:param generate_delta: Whether to generate a delta for each revision.
817
890
Permitted values are None, 'full' and 'partial'.
818
891
:param search: A user text search string.
819
:param file_ids: If non empty, only revisions matching one or more of
820
the file-ids are to be kept.
892
:param files: If non empty, only revisions matching one or more of
893
the files are to be kept.
821
894
:param direction: the direction in which view_revisions is sorted
822
895
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
825
898
# Convert view_revisions into (view, None, None) groups to fit with
826
899
# the standard interface here.
827
if type(view_revisions) == list:
900
if isinstance(view_revisions, list):
828
901
# A single batch conversion is faster than many incremental ones.
829
902
# As we have all the data, do a batch conversion.
830
903
nones = [None] * len(view_revisions)
831
log_rev_iterator = iter([zip(view_revisions, nones, nones)])
904
log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
834
907
for view in view_revisions:
876
952
def _match_filter(searchRE, rev):
878
'message': (rev.message,),
879
'committer': (rev.committer,),
880
'author': (rev.get_apparent_authors()),
881
'bugs': list(rev.iter_bugs())
883
strings[''] = [item for inner_list in strings.itervalues()
954
'message': (rev.message,),
955
'committer': (rev.committer,),
956
'author': (rev.get_apparent_authors()),
957
'bugs': list(rev.iter_bugs())
959
strings[''] = [item for inner_list in strings.values()
884
960
for item in inner_list]
885
for (k,v) in searchRE:
961
for k, v in searchRE:
886
962
if k in strings and not _match_any_filter(strings[k], v):
890
967
def _match_any_filter(strings, res):
891
return any([filter(None, map(re.search, strings)) for re in res])
968
return any(r.search(s) for r in res for s in strings)
893
971
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
894
fileids=None, direction='reverse'):
972
files=None, direction='reverse'):
895
973
"""Add revision deltas to a log iterator if needed.
897
975
:param branch: The branch being logged.
900
978
:param search: A user text search string.
901
979
:param log_rev_iterator: An input iterator containing all revisions that
902
980
could be displayed, in lists.
903
:param fileids: If non empty, only revisions matching one or more of
904
the file-ids are to be kept.
981
:param files: If non empty, only revisions matching one or more of
982
the files are to be kept.
905
983
:param direction: the direction in which view_revisions is sorted
906
984
:return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
909
if not generate_delta and not fileids:
987
if not generate_delta and not files:
910
988
return log_rev_iterator
911
989
return _generate_deltas(branch.repository, log_rev_iterator,
912
generate_delta, fileids, direction)
915
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
990
generate_delta, files, direction)
993
def _generate_deltas(repository, log_rev_iterator, delta_type, files,
917
995
"""Create deltas for each batch of revisions in log_rev_iterator.
919
997
If we're only generating deltas for the sake of filtering against
920
file-ids, we stop generating deltas once all file-ids reach the
998
files, we stop generating deltas once all files reach the
921
999
appropriate life-cycle point. If we're receiving data newest to
922
1000
oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
924
check_fileids = fileids is not None and len(fileids) > 0
926
fileid_set = set(fileids)
1002
check_files = files is not None and len(files) > 0
1004
file_set = set(files)
927
1005
if direction == 'reverse':
930
1008
stop_on = 'remove'
933
1011
for revs in log_rev_iterator:
934
# If we were matching against fileids and we've run out,
1012
# If we were matching against files and we've run out,
935
1013
# there's nothing left to do
936
if check_fileids and not fileid_set:
1014
if check_files and not file_set:
938
1016
revisions = [rev[1] for rev in revs]
940
if delta_type == 'full' and not check_fileids:
941
deltas = repository.get_deltas_for_revisions(revisions)
942
for rev, delta in izip(revs, deltas):
1018
if delta_type == 'full' and not check_files:
1019
deltas = repository.get_revision_deltas(revisions)
1020
for rev, delta in zip(revs, deltas):
943
1021
new_revs.append((rev[0], rev[1], delta))
945
deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
946
for rev, delta in izip(revs, deltas):
1023
deltas = repository.get_revision_deltas(
1024
revisions, specific_files=file_set)
1025
for rev, delta in zip(revs, deltas):
948
1027
if delta is None or not delta.has_changed():
951
_update_fileids(delta, fileid_set, stop_on)
1030
_update_files(delta, file_set, stop_on)
952
1031
if delta_type is None:
954
1033
elif delta_type == 'full':
968
def _update_fileids(delta, fileids, stop_on):
969
"""Update the set of file-ids to search based on file lifecycle events.
1047
def _update_files(delta, files, stop_on):
1048
"""Update the set of files to search based on file lifecycle events.
971
:param fileids: a set of fileids to update
972
:param stop_on: either 'add' or 'remove' - take file-ids out of the
973
fileids set once their add or remove entry is detected respectively
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
975
1054
if stop_on == 'add':
976
1055
for item in delta.added:
977
if item[1] in fileids:
978
fileids.remove(item[1])
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):
1066
files.add(item.path[0] + path[len(item.path[1]):])
979
1067
elif stop_on == 'delete':
980
1068
for item in delta.removed:
981
if item[1] in fileids:
982
fileids.remove(item[1])
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):
1079
files.add(item.path[1] + path[len(item.path[0]):])
985
1082
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1028
1123
def _get_revision_limits(branch, start_revision, end_revision):
1029
1124
"""Get and check revision limits.
1031
:param branch: The branch containing the revisions.
1033
:param start_revision: The first revision to be logged.
1034
For backwards compatibility this may be a mainline integer revno,
1035
but for merge revision support a RevisionInfo is expected.
1037
:param end_revision: The last revision to be logged.
1038
For backwards compatibility this may be a mainline integer revno,
1039
but for merge revision support a RevisionInfo is expected.
1126
:param branch: The branch containing the revisions.
1128
:param start_revision: The first revision to be logged, as a RevisionInfo.
1130
:param end_revision: The last revision to be logged, as a RevisionInfo
1041
1132
:return: (start_rev_id, end_rev_id) tuple.
1043
branch_revno, branch_rev_id = branch.last_revision_info()
1044
1134
start_rev_id = None
1045
if start_revision is 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:
1046
1142
start_revno = 1
1048
if isinstance(start_revision, revisionspec.RevisionInfo):
1049
start_rev_id = start_revision.rev_id
1050
start_revno = start_revision.revno or 1
1052
branch.check_real_revno(start_revision)
1053
start_revno = start_revision
1054
start_rev_id = branch.get_rev_id(start_revno)
1056
1144
end_rev_id = None
1057
if end_revision is None:
1058
end_revno = branch_revno
1060
if isinstance(end_revision, revisionspec.RevisionInfo):
1061
end_rev_id = end_revision.rev_id
1062
end_revno = end_revision.revno or branch_revno
1064
branch.check_real_revno(end_revision)
1065
end_revno = end_revision
1066
end_rev_id = branch.get_rev_id(end_revno)
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
1068
if branch_revno != 0:
1152
if branch.last_revision() != _mod_revision.NULL_REVISION:
1069
1153
if (start_rev_id == _mod_revision.NULL_REVISION
1070
or end_rev_id == _mod_revision.NULL_REVISION):
1071
raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1072
if start_revno > end_revno:
1073
raise errors.BzrCommandError(gettext("Start revision must be "
1074
"older than the end 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."))
1075
1160
return (start_rev_id, end_rev_id)
1636
1730
to_file = self.to_file
1638
1732
if revision.tags:
1639
tags = ' {%s}' % (', '.join(revision.tags))
1733
tags = ' {%s}' % (', '.join(sorted(revision.tags)))
1640
1734
to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1641
revision.revno or "", self.short_author(revision.rev),
1642
format_date(revision.rev.timestamp,
1643
revision.rev.timezone or 0,
1644
self.show_timezone, date_fmt="%Y-%m-%d",
1646
tags, self.merge_marker(revision)))
1647
self.show_properties(revision.rev, indent+offset)
1735
revision.revno or "", self.short_author(
1737
format_date(revision.rev.timestamp,
1738
revision.rev.timezone or 0,
1739
self.show_timezone, date_fmt="%Y-%m-%d",
1741
tags, self.merge_marker(revision)))
1742
self.show_properties(revision.rev, indent + offset)
1648
1743
if self.show_ids or revision.revno is None:
1649
1744
to_file.write(indent + offset + 'revision-id:%s\n'
1650
% (revision.rev.revision_id,))
1745
% (revision.rev.revision_id.decode('utf-8'),))
1651
1746
if not revision.rev.message:
1652
1747
to_file.write(indent + offset + '(no message)\n')
1866
1965
# This is the first index which is different between
1868
1967
base_idx = None
1869
for i in xrange(max(len(new_rh),
1968
for i in range(max(len(new_rh), len(old_rh))):
1871
1969
if (len(new_rh) <= i
1872
1970
or len(old_rh) <= i
1873
or new_rh[i] != old_rh[i]):
1971
or new_rh[i] != old_rh[i]):
1877
1975
if base_idx is None:
1878
1976
to_file.write('Nothing seems to have changed\n')
1880
## TODO: It might be nice to do something like show_log
1881
## and show the merged entries. But since this is the
1882
## removed revisions, it shouldn't be as important
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
1883
1981
if base_idx < len(old_rh):
1884
to_file.write('*'*60)
1982
to_file.write('*' * 60)
1885
1983
to_file.write('\nRemoved Revisions:\n')
1886
1984
for i in range(base_idx, len(old_rh)):
1887
1985
rev = branch.repository.get_revision(old_rh[i])
1888
lr = LogRevision(rev, i+1, 0, None)
1986
lr = LogRevision(rev, i + 1, 0, None)
1889
1987
lf.log_revision(lr)
1890
to_file.write('*'*60)
1988
to_file.write('*' * 60)
1891
1989
to_file.write('\n\n')
1892
1990
if base_idx < len(new_rh):
1893
1991
to_file.write('Added Revisions:\n')
1894
1992
show_log(branch,
1898
1995
direction='forward',
1899
start_revision=base_idx+1,
1900
end_revision=len(new_rh),
1996
start_revision=base_idx + 1,
1997
end_revision=len(new_rh))
1904
2000
def get_history_change(old_revision_id, new_revision_id, repository):
2007
2102
:param file_list: the list of paths given on the command line;
2008
2103
the first of these can be a branch location or a file path,
2009
2104
the remainder must be file paths
2010
:param add_cleanup: When the branch returned is read locked,
2011
an unlock call will be queued to the cleanup.
2105
:param exit_stack: When the branch returned is read locked,
2106
an unlock call will be queued to the exit stack.
2012
2107
:return: (branch, info_list, start_rev_info, end_rev_info) where
2013
info_list is a list of (relative_path, file_id, kind) tuples where
2108
info_list is a list of (relative_path, found, kind) tuples where
2014
2109
kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2015
2110
branch will be read-locked.
2017
from bzrlib.builtins import _get_revision_range
2112
from breezy.builtins import _get_revision_range
2018
2113
tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2020
add_cleanup(b.lock_read().unlock)
2115
exit_stack.enter_context(b.lock_read())
2021
2116
# XXX: It's damn messy converting a list of paths to relative paths when
2022
2117
# those paths might be deleted ones, they might be on a case-insensitive
2023
2118
# filesystem and/or they might be in silly locations (like another branch).
2040
2135
tree = b.basis_tree()
2042
2137
for fp in relpaths:
2043
file_id = tree.path2id(fp)
2044
kind = _get_kind_for_file_id(tree, file_id)
2138
kind = _get_kind_for_file(tree, fp)
2046
2140
# go back to when time began
2047
2141
if tree1 is None:
2049
2143
rev1 = b.get_rev_id(1)
2050
2144
except errors.NoSuchRevision:
2051
2145
# No history at all
2055
2148
tree1 = b.repository.revision_tree(rev1)
2057
file_id = tree1.path2id(fp)
2058
kind = _get_kind_for_file_id(tree1, file_id)
2059
info_list.append((fp, file_id, kind))
2150
kind = _get_kind_for_file(tree1, fp)
2151
info_list.append((fp, kind))
2061
2153
elif start_rev_info == end_rev_info:
2062
2154
# One revision given - file must exist in it
2063
2155
tree = b.repository.revision_tree(end_rev_info.rev_id)
2064
2156
for fp in relpaths:
2065
file_id = tree.path2id(fp)
2066
kind = _get_kind_for_file_id(tree, file_id)
2067
info_list.append((fp, file_id, kind))
2157
kind = _get_kind_for_file(tree, fp)
2158
info_list.append((fp, kind))
2070
2161
# Revision range given. Get the file-id from the end tree.
2086
2176
tree1 = b.repository.revision_tree(rev1)
2088
2178
tree1 = b.repository.revision_tree(rev_id)
2089
file_id = tree1.path2id(fp)
2090
kind = _get_kind_for_file_id(tree1, file_id)
2091
info_list.append((fp, file_id, kind))
2179
kind = _get_kind_for_file(tree1, fp)
2180
info_list.append((fp, kind))
2092
2181
return b, info_list, start_rev_info, end_rev_info
2095
def _get_kind_for_file_id(tree, file_id):
2096
"""Return the kind of a file-id or None if it doesn't exist."""
2097
if file_id is not None:
2098
return tree.kind(file_id)
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():
2188
return tree.stored_kind(path)
2189
except errors.NoSuchFile:
2103
2193
properties_handler_registry = registry.Registry()
2105
2195
# Use the properties handlers to print out bug information if available
2106
2198
def _bugs_properties_handler(revision):
2107
if revision.properties.has_key('bugs'):
2108
bug_lines = revision.properties['bugs'].split('\n')
2109
bug_rows = [line.split(' ', 1) for line in bug_lines]
2110
fixed_bug_urls = [row[0] for row in bug_rows if
2111
len(row) > 1 and row[1] == 'fixed']
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)
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)
2114
return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
2115
' '.join(fixed_bug_urls)}
2118
2217
properties_handler_registry.register('bugs_properties_handler',
2119
2218
_bugs_properties_handler)