1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
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
18
from cStringIO import StringIO
30
class TestCaseWithoutPropsHandler(tests.TestCaseWithTransport):
33
super(TestCaseWithoutPropsHandler, self).setUp()
34
# keep a reference to the "current" custom prop. handler registry
35
self.properties_handler_registry = log.properties_handler_registry
36
# clean up the registry in log
37
log.properties_handler_registry = registry.Registry()
40
super(TestCaseWithoutPropsHandler, self)._cleanup()
41
# restore the custom properties handler registry
42
log.properties_handler_registry = self.properties_handler_registry
45
class LogCatcher(log.LogFormatter):
46
"""Pull log messages into list rather than displaying them.
48
For ease of testing we save log messages here rather than actually
49
formatting them, so that we can precisely check the result without
50
being too dependent on the exact formatting.
52
We should also test the LogFormatter.
58
super(LogCatcher, self).__init__(to_file=None)
61
def log_revision(self, revision):
62
self.logs.append(revision)
65
class TestShowLog(tests.TestCaseWithTransport):
67
def checkDelta(self, delta, **kw):
68
"""Check the filenames touched by a delta are as expected.
70
Caller only have to pass in the list of files for each part, all
71
unspecified parts are considered empty (and checked as such).
73
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
74
# By default we expect an empty list
75
expected = kw.get(n, [])
76
# strip out only the path components
77
got = [x[0] for x in getattr(delta, n)]
78
self.assertEquals(expected, got)
80
def test_cur_revno(self):
81
wt = self.make_branch_and_tree('.')
85
wt.commit('empty commit')
86
log.show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
88
def assertInvalidRev(start, end):
89
self.assertRaises(errors.InvalidRevisionNumber,
91
start_revision=start, end_revision=end)
93
# Since there is a single revision in the branch all the combinations
95
assertInvalidRev(2, 1)
96
assertInvalidRev(1, 2)
97
assertInvalidRev(0, 2)
98
assertInvalidRev(1, 0)
99
assertInvalidRev(-1, 1)
100
assertInvalidRev(1, -1)
102
def test_empty_branch(self):
103
wt = self.make_branch_and_tree('.')
106
log.show_log(wt.branch, lf)
108
self.assertEquals(lf.logs, [])
110
def test_empty_commit(self):
111
wt = self.make_branch_and_tree('.')
113
wt.commit('empty commit')
115
log.show_log(wt.branch, lf, verbose=True)
116
self.assertEquals(len(lf.logs), 1)
117
self.assertEquals(lf.logs[0].revno, '1')
118
self.assertEquals(lf.logs[0].rev.message, 'empty commit')
119
self.checkDelta(lf.logs[0].delta)
121
def test_simple_commit(self):
122
wt = self.make_branch_and_tree('.')
123
wt.commit('empty commit')
124
self.build_tree(['hello'])
126
wt.commit('add one file',
127
committer=u'\u013d\xf3r\xe9m \xcdp\u0161\xfam '
128
u'<test@example.com>')
130
log.show_log(wt.branch, lf, verbose=True)
131
self.assertEquals(len(lf.logs), 2)
132
# first one is most recent
133
log_entry = lf.logs[0]
134
self.assertEquals(log_entry.revno, '2')
135
self.assertEquals(log_entry.rev.message, 'add one file')
136
self.checkDelta(log_entry.delta, added=['hello'])
138
def test_commit_message_with_control_chars(self):
139
wt = self.make_branch_and_tree('.')
140
msg = "All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
143
log.show_log(wt.branch, lf, verbose=True)
144
committed_msg = lf.logs[0].rev.message
145
self.assert_(msg != committed_msg)
146
self.assert_(len(committed_msg) > len(msg))
148
def test_commit_message_without_control_chars(self):
149
wt = self.make_branch_and_tree('.')
150
# escaped. As ElementTree apparently does some kind of
151
# newline conversion, neither LF (\x0A) nor CR (\x0D) are
152
# included in the test commit message, even though they are
153
# valid XML 1.0 characters.
154
msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
157
log.show_log(wt.branch, lf, verbose=True)
158
committed_msg = lf.logs[0].rev.message
159
self.assert_(msg == committed_msg)
161
def test_deltas_in_merge_revisions(self):
162
"""Check deltas created for both mainline and merge revisions"""
163
eq = self.assertEquals
164
wt = self.make_branch_and_tree('parent')
165
self.build_tree(['parent/file1', 'parent/file2', 'parent/file3'])
168
wt.commit(message='add file1 and file2')
169
self.run_bzr('branch parent child')
170
os.unlink('child/file1')
171
file('child/file2', 'wb').write('hello\n')
172
self.run_bzr(['commit', '-m', 'remove file1 and modify file2',
175
self.run_bzr('merge ../child')
176
wt.commit('merge child branch')
180
lf.supports_merge_revisions = True
181
log.show_log(b, lf, verbose=True)
182
self.assertEquals(len(lf.logs),3)
183
logentry = lf.logs[0]
184
self.assertEquals(logentry.revno, '2')
185
self.assertEquals(logentry.rev.message, 'merge child branch')
187
self.checkDelta(d, removed=['file1'], modified=['file2'])
188
logentry = lf.logs[1]
189
self.assertEquals(logentry.revno, '1.1.1')
190
self.assertEquals(logentry.rev.message, 'remove file1 and modify file2')
192
self.checkDelta(d, removed=['file1'], modified=['file2'])
193
logentry = lf.logs[2]
194
self.assertEquals(logentry.revno, '1')
195
self.assertEquals(logentry.rev.message, 'add file1 and file2')
197
self.checkDelta(d, added=['file1', 'file2'])
199
def test_merges_nonsupporting_formatter(self):
200
"""Tests that show_log will raise if the formatter doesn't
201
support merge revisions."""
202
wt = self.make_branch_and_memory_tree('.')
204
self.addCleanup(wt.unlock)
206
wt.commit('rev-1', rev_id='rev-1',
207
timestamp=1132586655, timezone=36000,
208
committer='Joe Foo <joe@foo.com>')
209
wt.commit('rev-merged', rev_id='rev-2a',
210
timestamp=1132586700, timezone=36000,
211
committer='Joe Foo <joe@foo.com>')
212
wt.set_parent_ids(['rev-1', 'rev-2a'])
213
wt.branch.set_last_revision_info(1, 'rev-1')
214
wt.commit('rev-2', rev_id='rev-2b',
215
timestamp=1132586800, timezone=36000,
216
committer='Joe Foo <joe@foo.com>')
217
logfile = self.make_utf8_encoded_stringio()
218
formatter = log.ShortLogFormatter(to_file=logfile)
221
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
222
rev = revspec.in_history(wtb)
223
self.assertRaises(errors.BzrCommandError, log.show_log, wtb, lf,
224
start_revision=rev, end_revision=rev)
227
def make_commits_with_trailing_newlines(wt):
228
"""Helper method for LogFormatter tests"""
231
open('a', 'wb').write('hello moto\n')
233
wt.commit('simple log message', rev_id='a1',
234
timestamp=1132586655.459960938, timezone=-6*3600,
235
committer='Joe Foo <joe@foo.com>')
236
open('b', 'wb').write('goodbye\n')
238
wt.commit('multiline\nlog\nmessage\n', rev_id='a2',
239
timestamp=1132586842.411175966, timezone=-6*3600,
240
committer='Joe Foo <joe@foo.com>',
241
author='Joe Bar <joe@bar.com>')
243
open('c', 'wb').write('just another manic monday\n')
245
wt.commit('single line with trailing newline\n', rev_id='a3',
246
timestamp=1132587176.835228920, timezone=-6*3600,
247
committer = 'Joe Foo <joe@foo.com>')
251
def normalize_log(log):
252
"""Replaces the variable lines of logs with fixed lines"""
253
author = 'author: Dolor Sit <test@example.com>'
254
committer = 'committer: Lorem Ipsum <test@example.com>'
255
lines = log.splitlines(True)
256
for idx,line in enumerate(lines):
257
stripped_line = line.lstrip()
258
indent = ' ' * (len(line) - len(stripped_line))
259
if stripped_line.startswith('author:'):
260
lines[idx] = indent + author + '\n'
261
elif stripped_line.startswith('committer:'):
262
lines[idx] = indent + committer + '\n'
263
elif stripped_line.startswith('timestamp:'):
264
lines[idx] = indent + 'timestamp: Just now\n'
265
return ''.join(lines)
268
class TestShortLogFormatter(tests.TestCaseWithTransport):
270
def test_trailing_newlines(self):
271
wt = self.make_branch_and_tree('.')
272
b = make_commits_with_trailing_newlines(wt)
273
sio = self.make_utf8_encoded_stringio()
274
lf = log.ShortLogFormatter(to_file=sio)
276
self.assertEqualDiff(sio.getvalue(), """\
277
3 Joe Foo\t2005-11-21
278
single line with trailing newline
280
2 Joe Bar\t2005-11-21
285
1 Joe Foo\t2005-11-21
290
def test_short_log_with_merges(self):
291
wt = self.make_branch_and_memory_tree('.')
293
self.addCleanup(wt.unlock)
295
wt.commit('rev-1', rev_id='rev-1',
296
timestamp=1132586655, timezone=36000,
297
committer='Joe Foo <joe@foo.com>')
298
wt.commit('rev-merged', rev_id='rev-2a',
299
timestamp=1132586700, timezone=36000,
300
committer='Joe Foo <joe@foo.com>')
301
wt.set_parent_ids(['rev-1', 'rev-2a'])
302
wt.branch.set_last_revision_info(1, 'rev-1')
303
wt.commit('rev-2', rev_id='rev-2b',
304
timestamp=1132586800, timezone=36000,
305
committer='Joe Foo <joe@foo.com>')
306
logfile = self.make_utf8_encoded_stringio()
307
formatter = log.ShortLogFormatter(to_file=logfile)
308
log.show_log(wt.branch, formatter)
309
self.assertEqualDiff(logfile.getvalue(), """\
310
2 Joe Foo\t2005-11-22 [merge]
313
1 Joe Foo\t2005-11-22
318
def test_short_log_single_merge_revision(self):
319
wt = self.make_branch_and_memory_tree('.')
321
self.addCleanup(wt.unlock)
323
wt.commit('rev-1', rev_id='rev-1',
324
timestamp=1132586655, timezone=36000,
325
committer='Joe Foo <joe@foo.com>')
326
wt.commit('rev-merged', rev_id='rev-2a',
327
timestamp=1132586700, timezone=36000,
328
committer='Joe Foo <joe@foo.com>')
329
wt.set_parent_ids(['rev-1', 'rev-2a'])
330
wt.branch.set_last_revision_info(1, 'rev-1')
331
wt.commit('rev-2', rev_id='rev-2b',
332
timestamp=1132586800, timezone=36000,
333
committer='Joe Foo <joe@foo.com>')
334
logfile = self.make_utf8_encoded_stringio()
335
formatter = log.ShortLogFormatter(to_file=logfile)
336
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
338
rev = revspec.in_history(wtb)
339
log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
340
self.assertEqualDiff(logfile.getvalue(), """\
341
1.1.1 Joe Foo\t2005-11-22
347
class TestLongLogFormatter(TestCaseWithoutPropsHandler):
349
def test_verbose_log(self):
350
"""Verbose log includes changed files
354
wt = self.make_branch_and_tree('.')
356
self.build_tree(['a'])
358
# XXX: why does a longer nick show up?
359
b.nick = 'test_verbose_log'
360
wt.commit(message='add a',
361
timestamp=1132711707,
363
committer='Lorem Ipsum <test@example.com>')
364
logfile = file('out.tmp', 'w+')
365
formatter = log.LongLogFormatter(to_file=logfile)
366
log.show_log(b, formatter, verbose=True)
369
log_contents = logfile.read()
370
self.assertEqualDiff(log_contents, '''\
371
------------------------------------------------------------
373
committer: Lorem Ipsum <test@example.com>
374
branch nick: test_verbose_log
375
timestamp: Wed 2005-11-23 12:08:27 +1000
382
def test_merges_are_indented_by_level(self):
383
wt = self.make_branch_and_tree('parent')
384
wt.commit('first post')
385
self.run_bzr('branch parent child')
386
self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
387
self.run_bzr('branch child smallerchild')
388
self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
391
self.run_bzr('merge ../smallerchild')
392
self.run_bzr(['commit', '-m', 'merge branch 2'])
393
os.chdir('../parent')
394
self.run_bzr('merge ../child')
395
wt.commit('merge branch 1')
397
sio = self.make_utf8_encoded_stringio()
398
lf = log.LongLogFormatter(to_file=sio)
399
log.show_log(b, lf, verbose=True)
400
the_log = normalize_log(sio.getvalue())
401
self.assertEqualDiff(the_log, """\
402
------------------------------------------------------------
404
committer: Lorem Ipsum <test@example.com>
409
------------------------------------------------------------
411
committer: Lorem Ipsum <test@example.com>
416
------------------------------------------------------------
418
committer: Lorem Ipsum <test@example.com>
419
branch nick: smallerchild
423
------------------------------------------------------------
425
committer: Lorem Ipsum <test@example.com>
430
------------------------------------------------------------
432
committer: Lorem Ipsum <test@example.com>
439
def test_verbose_merge_revisions_contain_deltas(self):
440
wt = self.make_branch_and_tree('parent')
441
self.build_tree(['parent/f1', 'parent/f2'])
443
wt.commit('first post')
444
self.run_bzr('branch parent child')
445
os.unlink('child/f1')
446
file('child/f2', 'wb').write('hello\n')
447
self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
450
self.run_bzr('merge ../child')
451
wt.commit('merge branch 1')
453
sio = self.make_utf8_encoded_stringio()
454
lf = log.LongLogFormatter(to_file=sio)
455
log.show_log(b, lf, verbose=True)
456
the_log = normalize_log(sio.getvalue())
457
self.assertEqualDiff(the_log, """\
458
------------------------------------------------------------
460
committer: Lorem Ipsum <test@example.com>
469
------------------------------------------------------------
471
committer: Lorem Ipsum <test@example.com>
475
removed f1 and modified f2
480
------------------------------------------------------------
482
committer: Lorem Ipsum <test@example.com>
492
def test_trailing_newlines(self):
493
wt = self.make_branch_and_tree('.')
494
b = make_commits_with_trailing_newlines(wt)
495
sio = self.make_utf8_encoded_stringio()
496
lf = log.LongLogFormatter(to_file=sio)
498
self.assertEqualDiff(sio.getvalue(), """\
499
------------------------------------------------------------
501
committer: Joe Foo <joe@foo.com>
503
timestamp: Mon 2005-11-21 09:32:56 -0600
505
single line with trailing newline
506
------------------------------------------------------------
508
author: Joe Bar <joe@bar.com>
509
committer: Joe Foo <joe@foo.com>
511
timestamp: Mon 2005-11-21 09:27:22 -0600
516
------------------------------------------------------------
518
committer: Joe Foo <joe@foo.com>
520
timestamp: Mon 2005-11-21 09:24:15 -0600
525
def test_author_in_log(self):
526
"""Log includes the author name if it's set in
527
the revision properties
529
wt = self.make_branch_and_tree('.')
531
self.build_tree(['a'])
533
b.nick = 'test_author_log'
534
wt.commit(message='add a',
535
timestamp=1132711707,
537
committer='Lorem Ipsum <test@example.com>',
538
author='John Doe <jdoe@example.com>')
540
formatter = log.LongLogFormatter(to_file=sio)
541
log.show_log(b, formatter)
542
self.assertEqualDiff(sio.getvalue(), '''\
543
------------------------------------------------------------
545
author: John Doe <jdoe@example.com>
546
committer: Lorem Ipsum <test@example.com>
547
branch nick: test_author_log
548
timestamp: Wed 2005-11-23 12:08:27 +1000
553
def test_properties_in_log(self):
554
"""Log includes the custom properties returned by the registered
557
wt = self.make_branch_and_tree('.')
559
self.build_tree(['a'])
561
b.nick = 'test_properties_in_log'
562
wt.commit(message='add a',
563
timestamp=1132711707,
565
committer='Lorem Ipsum <test@example.com>',
566
author='John Doe <jdoe@example.com>')
568
formatter = log.LongLogFormatter(to_file=sio)
570
def trivial_custom_prop_handler(revision):
571
return {'test_prop':'test_value'}
573
log.properties_handler_registry.register(
574
'trivial_custom_prop_handler',
575
trivial_custom_prop_handler)
576
log.show_log(b, formatter)
578
log.properties_handler_registry.remove(
579
'trivial_custom_prop_handler')
580
self.assertEqualDiff(sio.getvalue(), '''\
581
------------------------------------------------------------
583
test_prop: test_value
584
author: John Doe <jdoe@example.com>
585
committer: Lorem Ipsum <test@example.com>
586
branch nick: test_properties_in_log
587
timestamp: Wed 2005-11-23 12:08:27 +1000
592
def test_error_in_properties_handler(self):
593
"""Log includes the custom properties returned by the registered
596
wt = self.make_branch_and_tree('.')
598
self.build_tree(['a'])
600
b.nick = 'test_author_log'
601
wt.commit(message='add a',
602
timestamp=1132711707,
604
committer='Lorem Ipsum <test@example.com>',
605
author='John Doe <jdoe@example.com>',
606
revprops={'first_prop':'first_value'})
608
formatter = log.LongLogFormatter(to_file=sio)
610
def trivial_custom_prop_handler(revision):
611
raise StandardError("a test error")
613
log.properties_handler_registry.register(
614
'trivial_custom_prop_handler',
615
trivial_custom_prop_handler)
616
self.assertRaises(StandardError, log.show_log, b, formatter,)
618
log.properties_handler_registry.remove(
619
'trivial_custom_prop_handler')
621
def test_properties_handler_bad_argument(self):
622
wt = self.make_branch_and_tree('.')
624
self.build_tree(['a'])
626
b.nick = 'test_author_log'
627
wt.commit(message='add a',
628
timestamp=1132711707,
630
committer='Lorem Ipsum <test@example.com>',
631
author='John Doe <jdoe@example.com>',
632
revprops={'a_prop':'test_value'})
634
formatter = log.LongLogFormatter(to_file=sio)
636
def bad_argument_prop_handler(revision):
637
return {'custom_prop_name':revision.properties['a_prop']}
639
log.properties_handler_registry.register(
640
'bad_argument_prop_handler',
641
bad_argument_prop_handler)
643
self.assertRaises(AttributeError, formatter.show_properties,
646
revision = b.repository.get_revision(b.last_revision())
647
formatter.show_properties(revision, '')
648
self.assertEqualDiff(sio.getvalue(),
649
'''custom_prop_name: test_value\n''')
651
log.properties_handler_registry.remove(
652
'bad_argument_prop_handler')
655
class TestLineLogFormatter(tests.TestCaseWithTransport):
657
def test_line_log(self):
658
"""Line log should show revno
662
wt = self.make_branch_and_tree('.')
664
self.build_tree(['a'])
666
b.nick = 'test-line-log'
667
wt.commit(message='add a',
668
timestamp=1132711707,
670
committer='Line-Log-Formatter Tester <test@line.log>')
671
logfile = file('out.tmp', 'w+')
672
formatter = log.LineLogFormatter(to_file=logfile)
673
log.show_log(b, formatter)
676
log_contents = logfile.read()
677
self.assertEqualDiff(log_contents,
678
'1: Line-Log-Formatte... 2005-11-23 add a\n')
680
def test_trailing_newlines(self):
681
wt = self.make_branch_and_tree('.')
682
b = make_commits_with_trailing_newlines(wt)
683
sio = self.make_utf8_encoded_stringio()
684
lf = log.LineLogFormatter(to_file=sio)
686
self.assertEqualDiff(sio.getvalue(), """\
687
3: Joe Foo 2005-11-21 single line with trailing newline
688
2: Joe Bar 2005-11-21 multiline
689
1: Joe Foo 2005-11-21 simple log message
692
def test_line_log_single_merge_revision(self):
693
wt = self.make_branch_and_memory_tree('.')
695
self.addCleanup(wt.unlock)
697
wt.commit('rev-1', rev_id='rev-1',
698
timestamp=1132586655, timezone=36000,
699
committer='Joe Foo <joe@foo.com>')
700
wt.commit('rev-merged', rev_id='rev-2a',
701
timestamp=1132586700, timezone=36000,
702
committer='Joe Foo <joe@foo.com>')
703
wt.set_parent_ids(['rev-1', 'rev-2a'])
704
wt.branch.set_last_revision_info(1, 'rev-1')
705
wt.commit('rev-2', rev_id='rev-2b',
706
timestamp=1132586800, timezone=36000,
707
committer='Joe Foo <joe@foo.com>')
708
logfile = self.make_utf8_encoded_stringio()
709
formatter = log.LineLogFormatter(to_file=logfile)
710
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
712
rev = revspec.in_history(wtb)
713
log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
714
self.assertEqualDiff(logfile.getvalue(), """\
715
1.1.1: Joe Foo 2005-11-22 rev-merged
720
class TestGetViewRevisions(tests.TestCaseWithTransport):
722
def make_tree_with_commits(self):
723
"""Create a tree with well-known revision ids"""
724
wt = self.make_branch_and_tree('tree1')
725
wt.commit('commit one', rev_id='1')
726
wt.commit('commit two', rev_id='2')
727
wt.commit('commit three', rev_id='3')
728
mainline_revs = [None, '1', '2', '3']
729
rev_nos = {'1': 1, '2': 2, '3': 3}
730
return mainline_revs, rev_nos, wt
732
def make_tree_with_merges(self):
733
"""Create a tree with well-known revision ids and a merge"""
734
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
735
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
736
tree2.commit('four-a', rev_id='4a')
737
wt.merge_from_branch(tree2.branch)
738
wt.commit('four-b', rev_id='4b')
739
mainline_revs.append('4b')
742
return mainline_revs, rev_nos, wt
744
def make_tree_with_many_merges(self):
745
"""Create a tree with well-known revision ids"""
746
wt = self.make_branch_and_tree('tree1')
747
self.build_tree_contents([('tree1/f', '1\n')])
748
wt.add(['f'], ['f-id'])
749
wt.commit('commit one', rev_id='1')
750
wt.commit('commit two', rev_id='2')
752
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
753
self.build_tree_contents([('tree3/f', '1\n2\n3a\n')])
754
tree3.commit('commit three a', rev_id='3a')
756
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
757
tree2.merge_from_branch(tree3.branch)
758
tree2.commit('commit three b', rev_id='3b')
760
wt.merge_from_branch(tree2.branch)
761
wt.commit('commit three c', rev_id='3c')
762
tree2.commit('four-a', rev_id='4a')
764
wt.merge_from_branch(tree2.branch)
765
wt.commit('four-b', rev_id='4b')
767
mainline_revs = [None, '1', '2', '3c', '4b']
768
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
769
full_rev_nos_for_reference = {
772
'3a': '2.1.1', #first commit tree 3
773
'3b': '2.2.1', # first commit tree 2
774
'3c': '3', #merges 3b to main
775
'4a': '2.2.2', # second commit tree 2
776
'4b': '4', # merges 4a to main
778
return mainline_revs, rev_nos, wt
780
def test_get_view_revisions_forward(self):
781
"""Test the get_view_revisions method"""
782
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
784
self.addCleanup(wt.unlock)
785
revisions = list(log.get_view_revisions(
786
mainline_revs, rev_nos, wt.branch, 'forward'))
787
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
789
revisions2 = list(log.get_view_revisions(
790
mainline_revs, rev_nos, wt.branch, 'forward',
791
include_merges=False))
792
self.assertEqual(revisions, revisions2)
794
def test_get_view_revisions_reverse(self):
795
"""Test the get_view_revisions with reverse"""
796
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
798
self.addCleanup(wt.unlock)
799
revisions = list(log.get_view_revisions(
800
mainline_revs, rev_nos, wt.branch, 'reverse'))
801
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
803
revisions2 = list(log.get_view_revisions(
804
mainline_revs, rev_nos, wt.branch, 'reverse',
805
include_merges=False))
806
self.assertEqual(revisions, revisions2)
808
def test_get_view_revisions_merge(self):
809
"""Test get_view_revisions when there are merges"""
810
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
812
self.addCleanup(wt.unlock)
813
revisions = list(log.get_view_revisions(
814
mainline_revs, rev_nos, wt.branch, 'forward'))
815
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
816
('4b', '4', 0), ('4a', '3.1.1', 1)],
818
revisions = list(log.get_view_revisions(
819
mainline_revs, rev_nos, wt.branch, 'forward',
820
include_merges=False))
821
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
825
def test_get_view_revisions_merge_reverse(self):
826
"""Test get_view_revisions in reverse when there are merges"""
827
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
829
self.addCleanup(wt.unlock)
830
revisions = list(log.get_view_revisions(
831
mainline_revs, rev_nos, wt.branch, 'reverse'))
832
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
833
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
835
revisions = list(log.get_view_revisions(
836
mainline_revs, rev_nos, wt.branch, 'reverse',
837
include_merges=False))
838
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
842
def test_get_view_revisions_merge2(self):
843
"""Test get_view_revisions when there are merges"""
844
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
846
self.addCleanup(wt.unlock)
847
revisions = list(log.get_view_revisions(
848
mainline_revs, rev_nos, wt.branch, 'forward'))
849
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
850
('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
852
self.assertEqual(expected, revisions)
853
revisions = list(log.get_view_revisions(
854
mainline_revs, rev_nos, wt.branch, 'forward',
855
include_merges=False))
856
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
861
def test_file_id_for_range(self):
862
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
864
self.addCleanup(wt.unlock)
866
def rev_from_rev_id(revid, branch):
867
revspec = revisionspec.RevisionSpec.from_string('revid:%s' % revid)
868
return revspec.in_history(branch)
870
def view_revs(start_rev, end_rev, file_id, direction):
871
revs = log.calculate_view_revisions(
873
start_rev, # start_revision
874
end_rev, # end_revision
875
direction, # direction
876
file_id, # specific_fileid
877
True, # generate_merge_revisions
878
True, # allow_single_merge_revision
882
rev_3a = rev_from_rev_id('3a', wt.branch)
883
rev_4b = rev_from_rev_id('4b', wt.branch)
884
self.assertEquals([('3c', '3', 0), ('3a', '2.1.1', 1)],
885
view_revs(rev_3a, rev_4b, 'f-id', 'reverse'))
886
# Note that the depth is 0 for 3a because depths are normalized, but
887
# there is still a bug somewhere... most probably in
888
# _filter_revision_range and/or get_view_revisions still around a bad
889
# use of reverse_by_depth
890
self.assertEquals([('3a', '2.1.1', 0)],
891
view_revs(rev_3a, rev_4b, 'f-id', 'forward'))
894
class TestGetRevisionsTouchingFileID(tests.TestCaseWithTransport):
896
def create_tree_with_single_merge(self):
897
"""Create a branch with a moderate layout.
899
The revision graph looks like:
907
In this graph, A introduced files f1 and f2 and f3.
908
B modifies f1 and f3, and C modifies f2 and f3.
909
D merges the changes from B and C and resolves the conflict for f3.
911
# TODO: jam 20070218 This seems like it could really be done
912
# with make_branch_and_memory_tree() if we could just
913
# create the content of those files.
914
# TODO: jam 20070218 Another alternative is that we would really
915
# like to only create this tree 1 time for all tests that
916
# use it. Since 'log' only uses the tree in a readonly
917
# fashion, it seems a shame to regenerate an identical
918
# tree for each test.
919
tree = self.make_branch_and_tree('tree')
921
self.addCleanup(tree.unlock)
923
self.build_tree_contents([('tree/f1', 'A\n'),
927
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
928
tree.commit('A', rev_id='A')
930
self.build_tree_contents([('tree/f2', 'A\nC\n'),
931
('tree/f3', 'A\nC\n'),
933
tree.commit('C', rev_id='C')
934
# Revert back to A to build the other history.
935
tree.set_last_revision('A')
936
tree.branch.set_last_revision_info(1, 'A')
937
self.build_tree_contents([('tree/f1', 'A\nB\n'),
939
('tree/f3', 'A\nB\n'),
941
tree.commit('B', rev_id='B')
942
tree.set_parent_ids(['B', 'C'])
943
self.build_tree_contents([('tree/f1', 'A\nB\n'),
944
('tree/f2', 'A\nC\n'),
945
('tree/f3', 'A\nB\nC\n'),
947
tree.commit('D', rev_id='D')
949
# Switch to a read lock for this tree.
950
# We still have an addCleanup(tree.unlock) pending
955
def check_delta(self, delta, **kw):
956
"""Check the filenames touched by a delta are as expected.
958
Caller only have to pass in the list of files for each part, all
959
unspecified parts are considered empty (and checked as such).
961
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
962
# By default we expect an empty list
963
expected = kw.get(n, [])
964
# strip out only the path components
965
got = [x[0] for x in getattr(delta, n)]
966
self.assertEquals(expected, got)
968
def test_tree_with_single_merge(self):
969
"""Make sure the tree layout is correct."""
970
tree = self.create_tree_with_single_merge()
971
rev_A_tree = tree.branch.repository.revision_tree('A')
972
rev_B_tree = tree.branch.repository.revision_tree('B')
973
rev_C_tree = tree.branch.repository.revision_tree('C')
974
rev_D_tree = tree.branch.repository.revision_tree('D')
976
self.check_delta(rev_B_tree.changes_from(rev_A_tree),
977
modified=['f1', 'f3'])
979
self.check_delta(rev_C_tree.changes_from(rev_A_tree),
980
modified=['f2', 'f3'])
982
self.check_delta(rev_D_tree.changes_from(rev_B_tree),
983
modified=['f2', 'f3'])
985
self.check_delta(rev_D_tree.changes_from(rev_C_tree),
986
modified=['f1', 'f3'])
988
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
989
"""Ensure _filter_revisions_touching_file_id returns the right values.
991
Get the return value from _filter_revisions_touching_file_id and make
992
sure they are correct.
994
# The api for _filter_revisions_touching_file_id is a little crazy.
995
# So we do the setup here.
996
mainline = tree.branch.revision_history()
997
mainline.insert(0, None)
998
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
999
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
1001
actual_revs = log._filter_revisions_touching_file_id(
1004
list(view_revs_iter))
1005
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
1007
def test_file_id_f1(self):
1008
tree = self.create_tree_with_single_merge()
1009
# f1 should be marked as modified by revisions A and B
1010
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
1012
def test_file_id_f2(self):
1013
tree = self.create_tree_with_single_merge()
1014
# f2 should be marked as modified by revisions A, C, and D
1015
# because D merged the changes from C.
1016
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1018
def test_file_id_f3(self):
1019
tree = self.create_tree_with_single_merge()
1020
# f3 should be marked as modified by revisions A, B, C, and D
1021
self.assertAllRevisionsForFileID(tree, 'f3-id', ['D', 'C', 'B', 'A'])
1023
def test_file_id_with_ghosts(self):
1024
# This is testing bug #209948, where having a ghost would cause
1025
# _filter_revisions_touching_file_id() to fail.
1026
tree = self.create_tree_with_single_merge()
1027
# We need to add a revision, so switch back to a write-locked tree
1028
# (still a single addCleanup(tree.unlock) pending).
1031
first_parent = tree.last_revision()
1032
tree.set_parent_ids([first_parent, 'ghost-revision-id'])
1033
self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
1034
tree.commit('commit with a ghost', rev_id='XX')
1035
self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
1036
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1039
class TestShowChangedRevisions(tests.TestCaseWithTransport):
1041
def test_show_changed_revisions_verbose(self):
1042
tree = self.make_branch_and_tree('tree_a')
1043
self.build_tree(['tree_a/foo'])
1045
tree.commit('bar', rev_id='bar-id')
1046
s = self.make_utf8_encoded_stringio()
1047
log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
1048
self.assertContainsRe(s.getvalue(), 'bar')
1049
self.assertNotContainsRe(s.getvalue(), 'foo')
1052
class TestLogFormatter(tests.TestCase):
1054
def test_short_committer(self):
1055
rev = revision.Revision('a-id')
1056
rev.committer = 'John Doe <jdoe@example.com>'
1057
lf = log.LogFormatter(None)
1058
self.assertEqual('John Doe', lf.short_committer(rev))
1059
rev.committer = 'John Smith <jsmith@example.com>'
1060
self.assertEqual('John Smith', lf.short_committer(rev))
1061
rev.committer = 'John Smith'
1062
self.assertEqual('John Smith', lf.short_committer(rev))
1063
rev.committer = 'jsmith@example.com'
1064
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1065
rev.committer = '<jsmith@example.com>'
1066
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1067
rev.committer = 'John Smith jsmith@example.com'
1068
self.assertEqual('John Smith', lf.short_committer(rev))
1070
def test_short_author(self):
1071
rev = revision.Revision('a-id')
1072
rev.committer = 'John Doe <jdoe@example.com>'
1073
lf = log.LogFormatter(None)
1074
self.assertEqual('John Doe', lf.short_author(rev))
1075
rev.properties['author'] = 'John Smith <jsmith@example.com>'
1076
self.assertEqual('John Smith', lf.short_author(rev))
1077
rev.properties['author'] = 'John Smith'
1078
self.assertEqual('John Smith', lf.short_author(rev))
1079
rev.properties['author'] = 'jsmith@example.com'
1080
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1081
rev.properties['author'] = '<jsmith@example.com>'
1082
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1083
rev.properties['author'] = 'John Smith jsmith@example.com'
1084
self.assertEqual('John Smith', lf.short_author(rev))
1087
class TestReverseByDepth(tests.TestCase):
1088
"""Test reverse_by_depth behavior.
1090
This is used to present revisions in forward (oldest first) order in a nice
1093
The tests use lighter revision description to ease reading.
1096
def assertReversed(self, forward, backward):
1097
# Transform the descriptions to suit the API: tests use (revno, depth),
1098
# while the API expects (revid, revno, depth)
1099
def complete_revisions(l):
1100
"""Transform the description to suit the API.
1102
Tests use (revno, depth) whil the API expects (revid, revno, depth).
1103
Since the revid is arbitrary, we just duplicate revno
1105
return [ (r, r, d) for r, d in l]
1106
forward = complete_revisions(forward)
1107
backward= complete_revisions(backward)
1108
self.assertEqual(forward, log.reverse_by_depth(backward))
1111
def test_mainline_revisions(self):
1112
self.assertReversed([( '1', 0), ('2', 0)],
1113
[('2', 0), ('1', 0)])
1115
def test_merged_revisions(self):
1116
self.assertReversed([('1', 0), ('2', 0), ('2.2', 1), ('2.1', 1),],
1117
[('2', 0), ('2.1', 1), ('2.2', 1), ('1', 0),])
1118
def test_shifted_merged_revisions(self):
1119
"""Test irregular layout.
1121
Requesting revisions touching a file can produce "holes" in the depths.
1123
self.assertReversed([('1', 0), ('2', 0), ('1.1', 2), ('1.2', 2),],
1124
[('2', 0), ('1.2', 2), ('1.1', 2), ('1', 0),])
1126
def test_merged_without_child_revisions(self):
1127
"""Test irregular layout.
1129
Revision ranges can produce "holes" in the depths.
1131
# When a revision of higher depth doesn't follow one of lower depth, we
1132
# assume a lower depth one is virtually there
1133
self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1134
[('4', 4), ('3', 3), ('2', 2), ('1', 2),])
1135
# So we get the same order after reversing below even if the original
1136
# revisions are not in the same order.
1137
self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1138
[('3', 3), ('4', 4), ('2', 2), ('1', 2),])