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.assertEqual(expected, got)
80
def assertInvalidRevisonNumber(self, br, start, end):
82
self.assertRaises(errors.InvalidRevisionNumber,
84
start_revision=start, end_revision=end)
86
def test_cur_revno(self):
87
wt = self.make_branch_and_tree('.')
91
wt.commit('empty commit')
92
log.show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
94
# Since there is a single revision in the branch all the combinations
96
self.assertInvalidRevisonNumber(b, 2, 1)
97
self.assertInvalidRevisonNumber(b, 1, 2)
98
self.assertInvalidRevisonNumber(b, 0, 2)
99
self.assertInvalidRevisonNumber(b, 1, 0)
100
self.assertInvalidRevisonNumber(b, -1, 1)
101
self.assertInvalidRevisonNumber(b, 1, -1)
103
def test_empty_branch(self):
104
wt = self.make_branch_and_tree('.')
107
log.show_log(wt.branch, lf)
109
self.assertEqual([], lf.logs)
111
def test_empty_commit(self):
112
wt = self.make_branch_and_tree('.')
114
wt.commit('empty commit')
116
log.show_log(wt.branch, lf, verbose=True)
117
self.assertEqual(1, len(lf.logs))
118
self.assertEqual('1', lf.logs[0].revno)
119
self.assertEqual('empty commit', lf.logs[0].rev.message)
120
self.checkDelta(lf.logs[0].delta)
122
def test_simple_commit(self):
123
wt = self.make_branch_and_tree('.')
124
wt.commit('empty commit')
125
self.build_tree(['hello'])
127
wt.commit('add one file',
128
committer=u'\u013d\xf3r\xe9m \xcdp\u0161\xfam '
129
u'<test@example.com>')
131
log.show_log(wt.branch, lf, verbose=True)
132
self.assertEqual(2, len(lf.logs))
133
# first one is most recent
134
log_entry = lf.logs[0]
135
self.assertEqual('2', log_entry.revno)
136
self.assertEqual('add one file', log_entry.rev.message)
137
self.checkDelta(log_entry.delta, added=['hello'])
139
def test_commit_message_with_control_chars(self):
140
wt = self.make_branch_and_tree('.')
141
msg = u"All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
142
msg = msg.replace(u'\r', u'\n')
145
log.show_log(wt.branch, lf, verbose=True)
146
committed_msg = lf.logs[0].rev.message
147
self.assertNotEqual(msg, committed_msg)
148
self.assertTrue(len(committed_msg) > len(msg))
150
def test_commit_message_without_control_chars(self):
151
wt = self.make_branch_and_tree('.')
152
# escaped. As ElementTree apparently does some kind of
153
# newline conversion, neither LF (\x0A) nor CR (\x0D) are
154
# included in the test commit message, even though they are
155
# valid XML 1.0 characters.
156
msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
159
log.show_log(wt.branch, lf, verbose=True)
160
committed_msg = lf.logs[0].rev.message
161
self.assertEqual(msg, committed_msg)
163
def test_deltas_in_merge_revisions(self):
164
"""Check deltas created for both mainline and merge revisions"""
165
wt = self.make_branch_and_tree('parent')
166
self.build_tree(['parent/file1', 'parent/file2', 'parent/file3'])
169
wt.commit(message='add file1 and file2')
170
self.run_bzr('branch parent child')
171
os.unlink('child/file1')
172
file('child/file2', 'wb').write('hello\n')
173
self.run_bzr(['commit', '-m', 'remove file1 and modify file2',
176
self.run_bzr('merge ../child')
177
wt.commit('merge child branch')
181
lf.supports_merge_revisions = True
182
log.show_log(b, lf, verbose=True)
184
self.assertEqual(3, len(lf.logs))
186
logentry = lf.logs[0]
187
self.assertEqual('2', logentry.revno)
188
self.assertEqual('merge child branch', logentry.rev.message)
189
self.checkDelta(logentry.delta, removed=['file1'], modified=['file2'])
191
logentry = lf.logs[1]
192
self.assertEqual('1.1.1', logentry.revno)
193
self.assertEqual('remove file1 and modify file2', logentry.rev.message)
194
self.checkDelta(logentry.delta, removed=['file1'], modified=['file2'])
196
logentry = lf.logs[2]
197
self.assertEqual('1', logentry.revno)
198
self.assertEqual('add file1 and file2', logentry.rev.message)
199
self.checkDelta(logentry.delta, added=['file1', 'file2'])
201
def test_merges_nonsupporting_formatter(self):
202
"""Tests that show_log will raise if the formatter doesn't
203
support merge revisions."""
204
wt = self.make_branch_and_memory_tree('.')
206
self.addCleanup(wt.unlock)
208
wt.commit('rev-1', rev_id='rev-1',
209
timestamp=1132586655, timezone=36000,
210
committer='Joe Foo <joe@foo.com>')
211
wt.commit('rev-merged', rev_id='rev-2a',
212
timestamp=1132586700, timezone=36000,
213
committer='Joe Foo <joe@foo.com>')
214
wt.set_parent_ids(['rev-1', 'rev-2a'])
215
wt.branch.set_last_revision_info(1, 'rev-1')
216
wt.commit('rev-2', rev_id='rev-2b',
217
timestamp=1132586800, timezone=36000,
218
committer='Joe Foo <joe@foo.com>')
219
logfile = self.make_utf8_encoded_stringio()
220
formatter = log.ShortLogFormatter(to_file=logfile)
223
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
224
rev = revspec.in_history(wtb)
225
self.assertRaises(errors.BzrCommandError, log.show_log, wtb, lf,
226
start_revision=rev, end_revision=rev)
229
def make_commits_with_trailing_newlines(wt):
230
"""Helper method for LogFormatter tests"""
233
open('a', 'wb').write('hello moto\n')
235
wt.commit('simple log message', rev_id='a1',
236
timestamp=1132586655.459960938, timezone=-6*3600,
237
committer='Joe Foo <joe@foo.com>')
238
open('b', 'wb').write('goodbye\n')
240
wt.commit('multiline\nlog\nmessage\n', rev_id='a2',
241
timestamp=1132586842.411175966, timezone=-6*3600,
242
committer='Joe Foo <joe@foo.com>',
243
author='Joe Bar <joe@bar.com>')
245
open('c', 'wb').write('just another manic monday\n')
247
wt.commit('single line with trailing newline\n', rev_id='a3',
248
timestamp=1132587176.835228920, timezone=-6*3600,
249
committer = 'Joe Foo <joe@foo.com>')
253
def normalize_log(log):
254
"""Replaces the variable lines of logs with fixed lines"""
255
author = 'author: Dolor Sit <test@example.com>'
256
committer = 'committer: Lorem Ipsum <test@example.com>'
257
lines = log.splitlines(True)
258
for idx,line in enumerate(lines):
259
stripped_line = line.lstrip()
260
indent = ' ' * (len(line) - len(stripped_line))
261
if stripped_line.startswith('author:'):
262
lines[idx] = indent + author + '\n'
263
elif stripped_line.startswith('committer:'):
264
lines[idx] = indent + committer + '\n'
265
elif stripped_line.startswith('timestamp:'):
266
lines[idx] = indent + 'timestamp: Just now\n'
267
return ''.join(lines)
270
class TestShortLogFormatter(tests.TestCaseWithTransport):
272
def test_trailing_newlines(self):
273
wt = self.make_branch_and_tree('.')
274
b = make_commits_with_trailing_newlines(wt)
275
sio = self.make_utf8_encoded_stringio()
276
lf = log.ShortLogFormatter(to_file=sio)
278
self.assertEqualDiff("""\
279
3 Joe Foo\t2005-11-21
280
single line with trailing newline
282
2 Joe Bar\t2005-11-21
287
1 Joe Foo\t2005-11-21
293
def test_short_log_with_merges(self):
294
wt = self.make_branch_and_memory_tree('.')
296
self.addCleanup(wt.unlock)
298
wt.commit('rev-1', rev_id='rev-1',
299
timestamp=1132586655, timezone=36000,
300
committer='Joe Foo <joe@foo.com>')
301
wt.commit('rev-merged', rev_id='rev-2a',
302
timestamp=1132586700, timezone=36000,
303
committer='Joe Foo <joe@foo.com>')
304
wt.set_parent_ids(['rev-1', 'rev-2a'])
305
wt.branch.set_last_revision_info(1, 'rev-1')
306
wt.commit('rev-2', rev_id='rev-2b',
307
timestamp=1132586800, timezone=36000,
308
committer='Joe Foo <joe@foo.com>')
309
logfile = self.make_utf8_encoded_stringio()
310
formatter = log.ShortLogFormatter(to_file=logfile)
311
log.show_log(wt.branch, formatter)
312
self.assertEqualDiff("""\
313
2 Joe Foo\t2005-11-22 [merge]
316
1 Joe Foo\t2005-11-22
322
def test_short_log_with_merges_and_range(self):
323
wt = self.make_branch_and_memory_tree('.')
325
self.addCleanup(wt.unlock)
327
wt.commit('rev-1', rev_id='rev-1',
328
timestamp=1132586655, timezone=36000,
329
committer='Joe Foo <joe@foo.com>')
330
wt.commit('rev-merged', rev_id='rev-2a',
331
timestamp=1132586700, timezone=36000,
332
committer='Joe Foo <joe@foo.com>')
333
wt.branch.set_last_revision_info(1, 'rev-1')
334
wt.set_parent_ids(['rev-1', 'rev-2a'])
335
wt.commit('rev-2b', rev_id='rev-2b',
336
timestamp=1132586800, timezone=36000,
337
committer='Joe Foo <joe@foo.com>')
338
wt.commit('rev-3a', rev_id='rev-3a',
339
timestamp=1132586800, timezone=36000,
340
committer='Joe Foo <joe@foo.com>')
341
wt.branch.set_last_revision_info(2, 'rev-2b')
342
wt.set_parent_ids(['rev-2b', 'rev-3a'])
343
wt.commit('rev-3b', rev_id='rev-3b',
344
timestamp=1132586800, timezone=36000,
345
committer='Joe Foo <joe@foo.com>')
346
logfile = self.make_utf8_encoded_stringio()
347
formatter = log.ShortLogFormatter(to_file=logfile)
348
log.show_log(wt.branch, formatter,
349
start_revision=2, end_revision=3)
350
self.assertEqualDiff("""\
351
3 Joe Foo\t2005-11-22 [merge]
354
2 Joe Foo\t2005-11-22 [merge]
360
def test_short_log_single_merge_revision(self):
361
wt = self.make_branch_and_memory_tree('.')
363
self.addCleanup(wt.unlock)
365
wt.commit('rev-1', rev_id='rev-1',
366
timestamp=1132586655, timezone=36000,
367
committer='Joe Foo <joe@foo.com>')
368
wt.commit('rev-merged', rev_id='rev-2a',
369
timestamp=1132586700, timezone=36000,
370
committer='Joe Foo <joe@foo.com>')
371
wt.set_parent_ids(['rev-1', 'rev-2a'])
372
wt.branch.set_last_revision_info(1, 'rev-1')
373
wt.commit('rev-2', rev_id='rev-2b',
374
timestamp=1132586800, timezone=36000,
375
committer='Joe Foo <joe@foo.com>')
376
logfile = self.make_utf8_encoded_stringio()
377
formatter = log.ShortLogFormatter(to_file=logfile)
378
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
380
rev = revspec.in_history(wtb)
381
log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
382
self.assertEqualDiff("""\
383
1.1.1 Joe Foo\t2005-11-22
390
class TestLongLogFormatter(TestCaseWithoutPropsHandler):
392
def test_verbose_log(self):
393
"""Verbose log includes changed files
397
wt = self.make_branch_and_tree('.')
399
self.build_tree(['a'])
401
# XXX: why does a longer nick show up?
402
b.nick = 'test_verbose_log'
403
wt.commit(message='add a',
404
timestamp=1132711707,
406
committer='Lorem Ipsum <test@example.com>')
407
logfile = file('out.tmp', 'w+')
408
formatter = log.LongLogFormatter(to_file=logfile)
409
log.show_log(b, formatter, verbose=True)
412
log_contents = logfile.read()
413
self.assertEqualDiff('''\
414
------------------------------------------------------------
416
committer: Lorem Ipsum <test@example.com>
417
branch nick: test_verbose_log
418
timestamp: Wed 2005-11-23 12:08:27 +1000
426
def test_merges_are_indented_by_level(self):
427
wt = self.make_branch_and_tree('parent')
428
wt.commit('first post')
429
self.run_bzr('branch parent child')
430
self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
431
self.run_bzr('branch child smallerchild')
432
self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
435
self.run_bzr('merge ../smallerchild')
436
self.run_bzr(['commit', '-m', 'merge branch 2'])
437
os.chdir('../parent')
438
self.run_bzr('merge ../child')
439
wt.commit('merge branch 1')
441
sio = self.make_utf8_encoded_stringio()
442
lf = log.LongLogFormatter(to_file=sio)
443
log.show_log(b, lf, verbose=True)
444
the_log = normalize_log(sio.getvalue())
445
self.assertEqualDiff("""\
446
------------------------------------------------------------
448
committer: Lorem Ipsum <test@example.com>
453
------------------------------------------------------------
455
committer: Lorem Ipsum <test@example.com>
460
------------------------------------------------------------
462
committer: Lorem Ipsum <test@example.com>
463
branch nick: smallerchild
467
------------------------------------------------------------
469
committer: Lorem Ipsum <test@example.com>
474
------------------------------------------------------------
476
committer: Lorem Ipsum <test@example.com>
484
def test_verbose_merge_revisions_contain_deltas(self):
485
wt = self.make_branch_and_tree('parent')
486
self.build_tree(['parent/f1', 'parent/f2'])
488
wt.commit('first post')
489
self.run_bzr('branch parent child')
490
os.unlink('child/f1')
491
file('child/f2', 'wb').write('hello\n')
492
self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
495
self.run_bzr('merge ../child')
496
wt.commit('merge branch 1')
498
sio = self.make_utf8_encoded_stringio()
499
lf = log.LongLogFormatter(to_file=sio)
500
log.show_log(b, lf, verbose=True)
501
the_log = normalize_log(sio.getvalue())
502
self.assertEqualDiff("""\
503
------------------------------------------------------------
505
committer: Lorem Ipsum <test@example.com>
514
------------------------------------------------------------
516
committer: Lorem Ipsum <test@example.com>
520
removed f1 and modified f2
525
------------------------------------------------------------
527
committer: Lorem Ipsum <test@example.com>
538
def test_trailing_newlines(self):
539
wt = self.make_branch_and_tree('.')
540
b = make_commits_with_trailing_newlines(wt)
541
sio = self.make_utf8_encoded_stringio()
542
lf = log.LongLogFormatter(to_file=sio)
544
self.assertEqualDiff("""\
545
------------------------------------------------------------
547
committer: Joe Foo <joe@foo.com>
549
timestamp: Mon 2005-11-21 09:32:56 -0600
551
single line with trailing newline
552
------------------------------------------------------------
554
author: Joe Bar <joe@bar.com>
555
committer: Joe Foo <joe@foo.com>
557
timestamp: Mon 2005-11-21 09:27:22 -0600
562
------------------------------------------------------------
564
committer: Joe Foo <joe@foo.com>
566
timestamp: Mon 2005-11-21 09:24:15 -0600
572
def test_author_in_log(self):
573
"""Log includes the author name if it's set in
574
the revision properties
576
wt = self.make_branch_and_tree('.')
578
self.build_tree(['a'])
580
b.nick = 'test_author_log'
581
wt.commit(message='add a',
582
timestamp=1132711707,
584
committer='Lorem Ipsum <test@example.com>',
585
author='John Doe <jdoe@example.com>')
587
formatter = log.LongLogFormatter(to_file=sio)
588
log.show_log(b, formatter)
589
self.assertEqualDiff('''\
590
------------------------------------------------------------
592
author: John Doe <jdoe@example.com>
593
committer: Lorem Ipsum <test@example.com>
594
branch nick: test_author_log
595
timestamp: Wed 2005-11-23 12:08:27 +1000
601
def test_properties_in_log(self):
602
"""Log includes the custom properties returned by the registered
605
wt = self.make_branch_and_tree('.')
607
self.build_tree(['a'])
609
b.nick = 'test_properties_in_log'
610
wt.commit(message='add a',
611
timestamp=1132711707,
613
committer='Lorem Ipsum <test@example.com>',
614
author='John Doe <jdoe@example.com>')
616
formatter = log.LongLogFormatter(to_file=sio)
618
def trivial_custom_prop_handler(revision):
619
return {'test_prop':'test_value'}
621
log.properties_handler_registry.register(
622
'trivial_custom_prop_handler',
623
trivial_custom_prop_handler)
624
log.show_log(b, formatter)
626
log.properties_handler_registry.remove(
627
'trivial_custom_prop_handler')
628
self.assertEqualDiff('''\
629
------------------------------------------------------------
631
test_prop: test_value
632
author: John Doe <jdoe@example.com>
633
committer: Lorem Ipsum <test@example.com>
634
branch nick: test_properties_in_log
635
timestamp: Wed 2005-11-23 12:08:27 +1000
641
def test_error_in_properties_handler(self):
642
"""Log includes the custom properties returned by the registered
645
wt = self.make_branch_and_tree('.')
647
self.build_tree(['a'])
649
b.nick = 'test_author_log'
650
wt.commit(message='add a',
651
timestamp=1132711707,
653
committer='Lorem Ipsum <test@example.com>',
654
author='John Doe <jdoe@example.com>',
655
revprops={'first_prop':'first_value'})
657
formatter = log.LongLogFormatter(to_file=sio)
659
def trivial_custom_prop_handler(revision):
660
raise StandardError("a test error")
662
log.properties_handler_registry.register(
663
'trivial_custom_prop_handler',
664
trivial_custom_prop_handler)
665
self.assertRaises(StandardError, log.show_log, b, formatter,)
667
log.properties_handler_registry.remove(
668
'trivial_custom_prop_handler')
670
def test_properties_handler_bad_argument(self):
671
wt = self.make_branch_and_tree('.')
673
self.build_tree(['a'])
675
b.nick = 'test_author_log'
676
wt.commit(message='add a',
677
timestamp=1132711707,
679
committer='Lorem Ipsum <test@example.com>',
680
author='John Doe <jdoe@example.com>',
681
revprops={'a_prop':'test_value'})
683
formatter = log.LongLogFormatter(to_file=sio)
685
def bad_argument_prop_handler(revision):
686
return {'custom_prop_name':revision.properties['a_prop']}
688
log.properties_handler_registry.register(
689
'bad_argument_prop_handler',
690
bad_argument_prop_handler)
692
self.assertRaises(AttributeError, formatter.show_properties,
695
revision = b.repository.get_revision(b.last_revision())
696
formatter.show_properties(revision, '')
697
self.assertEqualDiff('''custom_prop_name: test_value\n''',
700
log.properties_handler_registry.remove(
701
'bad_argument_prop_handler')
704
class TestLineLogFormatter(tests.TestCaseWithTransport):
706
def test_line_log(self):
707
"""Line log should show revno
711
wt = self.make_branch_and_tree('.')
713
self.build_tree(['a'])
715
b.nick = 'test-line-log'
716
wt.commit(message='add a',
717
timestamp=1132711707,
719
committer='Line-Log-Formatter Tester <test@line.log>')
720
logfile = file('out.tmp', 'w+')
721
formatter = log.LineLogFormatter(to_file=logfile)
722
log.show_log(b, formatter)
725
log_contents = logfile.read()
726
self.assertEqualDiff('1: Line-Log-Formatte... 2005-11-23 add a\n',
729
def test_trailing_newlines(self):
730
wt = self.make_branch_and_tree('.')
731
b = make_commits_with_trailing_newlines(wt)
732
sio = self.make_utf8_encoded_stringio()
733
lf = log.LineLogFormatter(to_file=sio)
735
self.assertEqualDiff("""\
736
3: Joe Foo 2005-11-21 single line with trailing newline
737
2: Joe Bar 2005-11-21 multiline
738
1: Joe Foo 2005-11-21 simple log message
742
def test_line_log_single_merge_revision(self):
743
wt = self.make_branch_and_memory_tree('.')
745
self.addCleanup(wt.unlock)
747
wt.commit('rev-1', rev_id='rev-1',
748
timestamp=1132586655, timezone=36000,
749
committer='Joe Foo <joe@foo.com>')
750
wt.commit('rev-merged', rev_id='rev-2a',
751
timestamp=1132586700, timezone=36000,
752
committer='Joe Foo <joe@foo.com>')
753
wt.set_parent_ids(['rev-1', 'rev-2a'])
754
wt.branch.set_last_revision_info(1, 'rev-1')
755
wt.commit('rev-2', rev_id='rev-2b',
756
timestamp=1132586800, timezone=36000,
757
committer='Joe Foo <joe@foo.com>')
758
logfile = self.make_utf8_encoded_stringio()
759
formatter = log.LineLogFormatter(to_file=logfile)
760
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
762
rev = revspec.in_history(wtb)
763
log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
764
self.assertEqualDiff("""\
765
1.1.1: Joe Foo 2005-11-22 rev-merged
771
class TestGetViewRevisions(tests.TestCaseWithTransport):
773
def make_tree_with_commits(self):
774
"""Create a tree with well-known revision ids"""
775
wt = self.make_branch_and_tree('tree1')
776
wt.commit('commit one', rev_id='1')
777
wt.commit('commit two', rev_id='2')
778
wt.commit('commit three', rev_id='3')
779
mainline_revs = [None, '1', '2', '3']
780
rev_nos = {'1': 1, '2': 2, '3': 3}
781
return mainline_revs, rev_nos, wt
783
def make_tree_with_merges(self):
784
"""Create a tree with well-known revision ids and a merge"""
785
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
786
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
787
tree2.commit('four-a', rev_id='4a')
788
wt.merge_from_branch(tree2.branch)
789
wt.commit('four-b', rev_id='4b')
790
mainline_revs.append('4b')
793
return mainline_revs, rev_nos, wt
795
def make_tree_with_many_merges(self):
796
"""Create a tree with well-known revision ids"""
797
wt = self.make_branch_and_tree('tree1')
798
self.build_tree_contents([('tree1/f', '1\n')])
799
wt.add(['f'], ['f-id'])
800
wt.commit('commit one', rev_id='1')
801
wt.commit('commit two', rev_id='2')
803
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
804
self.build_tree_contents([('tree3/f', '1\n2\n3a\n')])
805
tree3.commit('commit three a', rev_id='3a')
807
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
808
tree2.merge_from_branch(tree3.branch)
809
tree2.commit('commit three b', rev_id='3b')
811
wt.merge_from_branch(tree2.branch)
812
wt.commit('commit three c', rev_id='3c')
813
tree2.commit('four-a', rev_id='4a')
815
wt.merge_from_branch(tree2.branch)
816
wt.commit('four-b', rev_id='4b')
818
mainline_revs = [None, '1', '2', '3c', '4b']
819
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
820
full_rev_nos_for_reference = {
823
'3a': '2.1.1', #first commit tree 3
824
'3b': '2.2.1', # first commit tree 2
825
'3c': '3', #merges 3b to main
826
'4a': '2.2.2', # second commit tree 2
827
'4b': '4', # merges 4a to main
829
return mainline_revs, rev_nos, wt
831
def test_get_view_revisions_forward(self):
832
"""Test the get_view_revisions method"""
833
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
835
self.addCleanup(wt.unlock)
836
revisions = list(log.get_view_revisions(
837
mainline_revs, rev_nos, wt.branch, 'forward'))
838
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
840
revisions2 = list(log.get_view_revisions(
841
mainline_revs, rev_nos, wt.branch, 'forward',
842
include_merges=False))
843
self.assertEqual(revisions, revisions2)
845
def test_get_view_revisions_reverse(self):
846
"""Test the get_view_revisions with reverse"""
847
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
849
self.addCleanup(wt.unlock)
850
revisions = list(log.get_view_revisions(
851
mainline_revs, rev_nos, wt.branch, 'reverse'))
852
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
854
revisions2 = list(log.get_view_revisions(
855
mainline_revs, rev_nos, wt.branch, 'reverse',
856
include_merges=False))
857
self.assertEqual(revisions, revisions2)
859
def test_get_view_revisions_merge(self):
860
"""Test get_view_revisions when there are merges"""
861
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
863
self.addCleanup(wt.unlock)
864
revisions = list(log.get_view_revisions(
865
mainline_revs, rev_nos, wt.branch, 'forward'))
866
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
867
('4b', '4', 0), ('4a', '3.1.1', 1)],
869
revisions = list(log.get_view_revisions(
870
mainline_revs, rev_nos, wt.branch, 'forward',
871
include_merges=False))
872
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
876
def test_get_view_revisions_merge_reverse(self):
877
"""Test get_view_revisions in reverse when there are merges"""
878
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
880
self.addCleanup(wt.unlock)
881
revisions = list(log.get_view_revisions(
882
mainline_revs, rev_nos, wt.branch, 'reverse'))
883
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
884
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
886
revisions = list(log.get_view_revisions(
887
mainline_revs, rev_nos, wt.branch, 'reverse',
888
include_merges=False))
889
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
893
def test_get_view_revisions_merge2(self):
894
"""Test get_view_revisions when there are merges"""
895
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
897
self.addCleanup(wt.unlock)
898
revisions = list(log.get_view_revisions(
899
mainline_revs, rev_nos, wt.branch, 'forward'))
900
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
901
('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
903
self.assertEqual(expected, revisions)
904
revisions = list(log.get_view_revisions(
905
mainline_revs, rev_nos, wt.branch, 'forward',
906
include_merges=False))
907
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
912
def test_file_id_for_range(self):
913
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
915
self.addCleanup(wt.unlock)
917
def rev_from_rev_id(revid, branch):
918
revspec = revisionspec.RevisionSpec.from_string('revid:%s' % revid)
919
return revspec.in_history(branch)
921
def view_revs(start_rev, end_rev, file_id, direction):
922
revs = log.calculate_view_revisions(
924
start_rev, # start_revision
925
end_rev, # end_revision
926
direction, # direction
927
file_id, # specific_fileid
928
True, # generate_merge_revisions
929
True, # allow_single_merge_revision
933
rev_3a = rev_from_rev_id('3a', wt.branch)
934
rev_4b = rev_from_rev_id('4b', wt.branch)
935
self.assertEqual([('3c', '3', 0), ('3a', '2.1.1', 1)],
936
view_revs(rev_3a, rev_4b, 'f-id', 'reverse'))
937
# Note that the depth is 0 for 3a because depths are normalized, but
938
# there is still a bug somewhere... most probably in
939
# _filter_revision_range and/or get_view_revisions still around a bad
940
# use of reverse_by_depth
941
self.assertEqual([('3a', '2.1.1', 0)],
942
view_revs(rev_3a, rev_4b, 'f-id', 'forward'))
945
class TestGetRevisionsTouchingFileID(tests.TestCaseWithTransport):
947
def create_tree_with_single_merge(self):
948
"""Create a branch with a moderate layout.
950
The revision graph looks like:
958
In this graph, A introduced files f1 and f2 and f3.
959
B modifies f1 and f3, and C modifies f2 and f3.
960
D merges the changes from B and C and resolves the conflict for f3.
962
# TODO: jam 20070218 This seems like it could really be done
963
# with make_branch_and_memory_tree() if we could just
964
# create the content of those files.
965
# TODO: jam 20070218 Another alternative is that we would really
966
# like to only create this tree 1 time for all tests that
967
# use it. Since 'log' only uses the tree in a readonly
968
# fashion, it seems a shame to regenerate an identical
969
# tree for each test.
970
tree = self.make_branch_and_tree('tree')
972
self.addCleanup(tree.unlock)
974
self.build_tree_contents([('tree/f1', 'A\n'),
978
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
979
tree.commit('A', rev_id='A')
981
self.build_tree_contents([('tree/f2', 'A\nC\n'),
982
('tree/f3', 'A\nC\n'),
984
tree.commit('C', rev_id='C')
985
# Revert back to A to build the other history.
986
tree.set_last_revision('A')
987
tree.branch.set_last_revision_info(1, 'A')
988
self.build_tree_contents([('tree/f1', 'A\nB\n'),
990
('tree/f3', 'A\nB\n'),
992
tree.commit('B', rev_id='B')
993
tree.set_parent_ids(['B', 'C'])
994
self.build_tree_contents([('tree/f1', 'A\nB\n'),
995
('tree/f2', 'A\nC\n'),
996
('tree/f3', 'A\nB\nC\n'),
998
tree.commit('D', rev_id='D')
1000
# Switch to a read lock for this tree.
1001
# We still have an addCleanup(tree.unlock) pending
1006
def check_delta(self, delta, **kw):
1007
"""Check the filenames touched by a delta are as expected.
1009
Caller only have to pass in the list of files for each part, all
1010
unspecified parts are considered empty (and checked as such).
1012
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
1013
# By default we expect an empty list
1014
expected = kw.get(n, [])
1015
# strip out only the path components
1016
got = [x[0] for x in getattr(delta, n)]
1017
self.assertEqual(expected, got)
1019
def test_tree_with_single_merge(self):
1020
"""Make sure the tree layout is correct."""
1021
tree = self.create_tree_with_single_merge()
1022
rev_A_tree = tree.branch.repository.revision_tree('A')
1023
rev_B_tree = tree.branch.repository.revision_tree('B')
1024
rev_C_tree = tree.branch.repository.revision_tree('C')
1025
rev_D_tree = tree.branch.repository.revision_tree('D')
1027
self.check_delta(rev_B_tree.changes_from(rev_A_tree),
1028
modified=['f1', 'f3'])
1030
self.check_delta(rev_C_tree.changes_from(rev_A_tree),
1031
modified=['f2', 'f3'])
1033
self.check_delta(rev_D_tree.changes_from(rev_B_tree),
1034
modified=['f2', 'f3'])
1036
self.check_delta(rev_D_tree.changes_from(rev_C_tree),
1037
modified=['f1', 'f3'])
1039
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
1040
"""Ensure _filter_revisions_touching_file_id returns the right values.
1042
Get the return value from _filter_revisions_touching_file_id and make
1043
sure they are correct.
1045
# The api for _filter_revisions_touching_file_id is a little crazy.
1046
# So we do the setup here.
1047
mainline = tree.branch.revision_history()
1048
mainline.insert(0, None)
1049
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
1050
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
1052
actual_revs = log._filter_revisions_touching_file_id(
1055
list(view_revs_iter))
1056
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
1058
def test_file_id_f1(self):
1059
tree = self.create_tree_with_single_merge()
1060
# f1 should be marked as modified by revisions A and B
1061
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
1063
def test_file_id_f2(self):
1064
tree = self.create_tree_with_single_merge()
1065
# f2 should be marked as modified by revisions A, C, and D
1066
# because D merged the changes from C.
1067
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1069
def test_file_id_f3(self):
1070
tree = self.create_tree_with_single_merge()
1071
# f3 should be marked as modified by revisions A, B, C, and D
1072
self.assertAllRevisionsForFileID(tree, 'f3-id', ['D', 'C', 'B', 'A'])
1074
def test_file_id_with_ghosts(self):
1075
# This is testing bug #209948, where having a ghost would cause
1076
# _filter_revisions_touching_file_id() to fail.
1077
tree = self.create_tree_with_single_merge()
1078
# We need to add a revision, so switch back to a write-locked tree
1079
# (still a single addCleanup(tree.unlock) pending).
1082
first_parent = tree.last_revision()
1083
tree.set_parent_ids([first_parent, 'ghost-revision-id'])
1084
self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
1085
tree.commit('commit with a ghost', rev_id='XX')
1086
self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
1087
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1090
class TestShowChangedRevisions(tests.TestCaseWithTransport):
1092
def test_show_changed_revisions_verbose(self):
1093
tree = self.make_branch_and_tree('tree_a')
1094
self.build_tree(['tree_a/foo'])
1096
tree.commit('bar', rev_id='bar-id')
1097
s = self.make_utf8_encoded_stringio()
1098
log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
1099
self.assertContainsRe(s.getvalue(), 'bar')
1100
self.assertNotContainsRe(s.getvalue(), 'foo')
1103
class TestLogFormatter(tests.TestCase):
1105
def test_short_committer(self):
1106
rev = revision.Revision('a-id')
1107
rev.committer = 'John Doe <jdoe@example.com>'
1108
lf = log.LogFormatter(None)
1109
self.assertEqual('John Doe', lf.short_committer(rev))
1110
rev.committer = 'John Smith <jsmith@example.com>'
1111
self.assertEqual('John Smith', lf.short_committer(rev))
1112
rev.committer = 'John Smith'
1113
self.assertEqual('John Smith', lf.short_committer(rev))
1114
rev.committer = 'jsmith@example.com'
1115
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1116
rev.committer = '<jsmith@example.com>'
1117
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1118
rev.committer = 'John Smith jsmith@example.com'
1119
self.assertEqual('John Smith', lf.short_committer(rev))
1121
def test_short_author(self):
1122
rev = revision.Revision('a-id')
1123
rev.committer = 'John Doe <jdoe@example.com>'
1124
lf = log.LogFormatter(None)
1125
self.assertEqual('John Doe', lf.short_author(rev))
1126
rev.properties['author'] = 'John Smith <jsmith@example.com>'
1127
self.assertEqual('John Smith', lf.short_author(rev))
1128
rev.properties['author'] = 'John Smith'
1129
self.assertEqual('John Smith', lf.short_author(rev))
1130
rev.properties['author'] = 'jsmith@example.com'
1131
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1132
rev.properties['author'] = '<jsmith@example.com>'
1133
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1134
rev.properties['author'] = 'John Smith jsmith@example.com'
1135
self.assertEqual('John Smith', lf.short_author(rev))
1138
class TestReverseByDepth(tests.TestCase):
1139
"""Test reverse_by_depth behavior.
1141
This is used to present revisions in forward (oldest first) order in a nice
1144
The tests use lighter revision description to ease reading.
1147
def assertReversed(self, forward, backward):
1148
# Transform the descriptions to suit the API: tests use (revno, depth),
1149
# while the API expects (revid, revno, depth)
1150
def complete_revisions(l):
1151
"""Transform the description to suit the API.
1153
Tests use (revno, depth) whil the API expects (revid, revno, depth).
1154
Since the revid is arbitrary, we just duplicate revno
1156
return [ (r, r, d) for r, d in l]
1157
forward = complete_revisions(forward)
1158
backward= complete_revisions(backward)
1159
self.assertEqual(forward, log.reverse_by_depth(backward))
1162
def test_mainline_revisions(self):
1163
self.assertReversed([( '1', 0), ('2', 0)],
1164
[('2', 0), ('1', 0)])
1166
def test_merged_revisions(self):
1167
self.assertReversed([('1', 0), ('2', 0), ('2.2', 1), ('2.1', 1),],
1168
[('2', 0), ('2.1', 1), ('2.2', 1), ('1', 0),])
1169
def test_shifted_merged_revisions(self):
1170
"""Test irregular layout.
1172
Requesting revisions touching a file can produce "holes" in the depths.
1174
self.assertReversed([('1', 0), ('2', 0), ('1.1', 2), ('1.2', 2),],
1175
[('2', 0), ('1.2', 2), ('1.1', 2), ('1', 0),])
1177
def test_merged_without_child_revisions(self):
1178
"""Test irregular layout.
1180
Revision ranges can produce "holes" in the depths.
1182
# When a revision of higher depth doesn't follow one of lower depth, we
1183
# assume a lower depth one is virtually there
1184
self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1185
[('4', 4), ('3', 3), ('2', 2), ('1', 2),])
1186
# So we get the same order after reversing below even if the original
1187
# revisions are not in the same order.
1188
self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1189
[('3', 3), ('4', 4), ('2', 2), ('1', 2),])
1192
class TestHistoryChange(tests.TestCaseWithTransport):
1194
def setup_a_tree(self):
1195
tree = self.make_branch_and_tree('tree')
1197
self.addCleanup(tree.unlock)
1198
tree.commit('1a', rev_id='1a')
1199
tree.commit('2a', rev_id='2a')
1200
tree.commit('3a', rev_id='3a')
1203
def setup_ab_tree(self):
1204
tree = self.setup_a_tree()
1205
tree.set_last_revision('1a')
1206
tree.branch.set_last_revision_info(1, '1a')
1207
tree.commit('2b', rev_id='2b')
1208
tree.commit('3b', rev_id='3b')
1211
def setup_ac_tree(self):
1212
tree = self.setup_a_tree()
1213
tree.set_last_revision(revision.NULL_REVISION)
1214
tree.branch.set_last_revision_info(0, revision.NULL_REVISION)
1215
tree.commit('1c', rev_id='1c')
1216
tree.commit('2c', rev_id='2c')
1217
tree.commit('3c', rev_id='3c')
1220
def test_all_new(self):
1221
tree = self.setup_ab_tree()
1222
old, new = log.get_history_change('1a', '3a', tree.branch.repository)
1223
self.assertEqual([], old)
1224
self.assertEqual(['2a', '3a'], new)
1226
def test_all_old(self):
1227
tree = self.setup_ab_tree()
1228
old, new = log.get_history_change('3a', '1a', tree.branch.repository)
1229
self.assertEqual([], new)
1230
self.assertEqual(['2a', '3a'], old)
1232
def test_null_old(self):
1233
tree = self.setup_ab_tree()
1234
old, new = log.get_history_change(revision.NULL_REVISION,
1235
'3a', tree.branch.repository)
1236
self.assertEqual([], old)
1237
self.assertEqual(['1a', '2a', '3a'], new)
1239
def test_null_new(self):
1240
tree = self.setup_ab_tree()
1241
old, new = log.get_history_change('3a', revision.NULL_REVISION,
1242
tree.branch.repository)
1243
self.assertEqual([], new)
1244
self.assertEqual(['1a', '2a', '3a'], old)
1246
def test_diverged(self):
1247
tree = self.setup_ab_tree()
1248
old, new = log.get_history_change('3a', '3b', tree.branch.repository)
1249
self.assertEqual(old, ['2a', '3a'])
1250
self.assertEqual(new, ['2b', '3b'])
1252
def test_unrelated(self):
1253
tree = self.setup_ac_tree()
1254
old, new = log.get_history_change('3a', '3c', tree.branch.repository)
1255
self.assertEqual(old, ['1a', '2a', '3a'])
1256
self.assertEqual(new, ['1c', '2c', '3c'])
1258
def test_show_branch_change(self):
1259
tree = self.setup_ab_tree()
1261
log.show_branch_change(tree.branch, s, 3, '3a')
1262
self.assertContainsRe(s.getvalue(),
1263
'[*]{60}\nRemoved Revisions:\n(.|\n)*2a(.|\n)*3a(.|\n)*'
1264
'[*]{60}\n\nAdded Revisions:\n(.|\n)*2b(.|\n)*3b')
1266
def test_show_branch_change_no_change(self):
1267
tree = self.setup_ab_tree()
1269
log.show_branch_change(tree.branch, s, 3, '3b')
1270
self.assertEqual(s.getvalue(),
1271
'Nothing seems to have changed\n')
1273
def test_show_branch_change_no_old(self):
1274
tree = self.setup_ab_tree()
1276
log.show_branch_change(tree.branch, s, 2, '2b')
1277
self.assertContainsRe(s.getvalue(), 'Added Revisions:')
1278
self.assertNotContainsRe(s.getvalue(), 'Removed Revisions:')
1280
def test_show_branch_change_no_new(self):
1281
tree = self.setup_ab_tree()
1282
tree.branch.set_last_revision_info(2, '2b')
1284
log.show_branch_change(tree.branch, s, 3, '3b')
1285
self.assertContainsRe(s.getvalue(), 'Removed Revisions:')
1286
self.assertNotContainsRe(s.getvalue(), 'Added Revisions:')