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 _prepare_tree_with_merges(self, with_tags=False):
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>')
311
branch.tags.set_tag('v0.2', 'rev-2b')
312
wt.commit('rev-3', rev_id='rev-3',
313
timestamp=1132586900, timezone=36000,
314
committer='Jane Foo <jane@foo.com>')
315
branch.tags.set_tag('v1.0rc1', 'rev-3')
316
branch.tags.set_tag('v1.0', 'rev-3')
319
def test_short_log_with_merges(self):
320
wt = self._prepare_tree_with_merges()
321
logfile = self.make_utf8_encoded_stringio()
322
formatter = log.ShortLogFormatter(to_file=logfile)
323
log.show_log(wt.branch, formatter)
324
self.assertEqualDiff("""\
325
2 Joe Foo\t2005-11-22 [merge]
328
1 Joe Foo\t2005-11-22
334
def test_short_log_with_merges_and_range(self):
335
wt = self.make_branch_and_memory_tree('.')
337
self.addCleanup(wt.unlock)
339
wt.commit('rev-1', rev_id='rev-1',
340
timestamp=1132586655, timezone=36000,
341
committer='Joe Foo <joe@foo.com>')
342
wt.commit('rev-merged', rev_id='rev-2a',
343
timestamp=1132586700, timezone=36000,
344
committer='Joe Foo <joe@foo.com>')
345
wt.branch.set_last_revision_info(1, 'rev-1')
346
wt.set_parent_ids(['rev-1', 'rev-2a'])
347
wt.commit('rev-2b', rev_id='rev-2b',
348
timestamp=1132586800, timezone=36000,
349
committer='Joe Foo <joe@foo.com>')
350
wt.commit('rev-3a', rev_id='rev-3a',
351
timestamp=1132586800, timezone=36000,
352
committer='Joe Foo <joe@foo.com>')
353
wt.branch.set_last_revision_info(2, 'rev-2b')
354
wt.set_parent_ids(['rev-2b', 'rev-3a'])
355
wt.commit('rev-3b', rev_id='rev-3b',
356
timestamp=1132586800, timezone=36000,
357
committer='Joe Foo <joe@foo.com>')
358
logfile = self.make_utf8_encoded_stringio()
359
formatter = log.ShortLogFormatter(to_file=logfile)
360
log.show_log(wt.branch, formatter,
361
start_revision=2, end_revision=3)
362
self.assertEqualDiff("""\
363
3 Joe Foo\t2005-11-22 [merge]
366
2 Joe Foo\t2005-11-22 [merge]
372
def test_short_log_with_tags(self):
373
wt = self._prepare_tree_with_merges(with_tags=True)
374
logfile = self.make_utf8_encoded_stringio()
375
formatter = log.ShortLogFormatter(to_file=logfile)
376
log.show_log(wt.branch, formatter)
377
self.assertEqualDiff("""\
378
3 Jane Foo\t2005-11-22 {v1.0, v1.0rc1}
381
2 Joe Foo\t2005-11-22 {v0.2} [merge]
384
1 Joe Foo\t2005-11-22
390
def test_short_log_single_merge_revision(self):
391
wt = self.make_branch_and_memory_tree('.')
393
self.addCleanup(wt.unlock)
395
wt.commit('rev-1', rev_id='rev-1',
396
timestamp=1132586655, timezone=36000,
397
committer='Joe Foo <joe@foo.com>')
398
wt.commit('rev-merged', rev_id='rev-2a',
399
timestamp=1132586700, timezone=36000,
400
committer='Joe Foo <joe@foo.com>')
401
wt.set_parent_ids(['rev-1', 'rev-2a'])
402
wt.branch.set_last_revision_info(1, 'rev-1')
403
wt.commit('rev-2', rev_id='rev-2b',
404
timestamp=1132586800, timezone=36000,
405
committer='Joe Foo <joe@foo.com>')
406
logfile = self.make_utf8_encoded_stringio()
407
formatter = log.ShortLogFormatter(to_file=logfile)
408
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
410
rev = revspec.in_history(wtb)
411
log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
412
self.assertEqualDiff("""\
413
1.1.1 Joe Foo\t2005-11-22
420
class TestLongLogFormatter(TestCaseWithoutPropsHandler):
422
def test_verbose_log(self):
423
"""Verbose log includes changed files
427
wt = self.make_branch_and_tree('.')
429
self.build_tree(['a'])
431
# XXX: why does a longer nick show up?
432
b.nick = 'test_verbose_log'
433
wt.commit(message='add a',
434
timestamp=1132711707,
436
committer='Lorem Ipsum <test@example.com>')
437
logfile = file('out.tmp', 'w+')
438
formatter = log.LongLogFormatter(to_file=logfile)
439
log.show_log(b, formatter, verbose=True)
442
log_contents = logfile.read()
443
self.assertEqualDiff('''\
444
------------------------------------------------------------
446
committer: Lorem Ipsum <test@example.com>
447
branch nick: test_verbose_log
448
timestamp: Wed 2005-11-23 12:08:27 +1000
456
def test_merges_are_indented_by_level(self):
457
wt = self.make_branch_and_tree('parent')
458
wt.commit('first post')
459
self.run_bzr('branch parent child')
460
self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
461
self.run_bzr('branch child smallerchild')
462
self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
465
self.run_bzr('merge ../smallerchild')
466
self.run_bzr(['commit', '-m', 'merge branch 2'])
467
os.chdir('../parent')
468
self.run_bzr('merge ../child')
469
wt.commit('merge branch 1')
471
sio = self.make_utf8_encoded_stringio()
472
lf = log.LongLogFormatter(to_file=sio)
473
log.show_log(b, lf, verbose=True)
474
the_log = normalize_log(sio.getvalue())
475
self.assertEqualDiff("""\
476
------------------------------------------------------------
478
committer: Lorem Ipsum <test@example.com>
483
------------------------------------------------------------
485
committer: Lorem Ipsum <test@example.com>
490
------------------------------------------------------------
492
committer: Lorem Ipsum <test@example.com>
493
branch nick: smallerchild
497
------------------------------------------------------------
499
committer: Lorem Ipsum <test@example.com>
504
------------------------------------------------------------
506
committer: Lorem Ipsum <test@example.com>
514
def test_verbose_merge_revisions_contain_deltas(self):
515
wt = self.make_branch_and_tree('parent')
516
self.build_tree(['parent/f1', 'parent/f2'])
518
wt.commit('first post')
519
self.run_bzr('branch parent child')
520
os.unlink('child/f1')
521
file('child/f2', 'wb').write('hello\n')
522
self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
525
self.run_bzr('merge ../child')
526
wt.commit('merge branch 1')
528
sio = self.make_utf8_encoded_stringio()
529
lf = log.LongLogFormatter(to_file=sio)
530
log.show_log(b, lf, verbose=True)
531
the_log = normalize_log(sio.getvalue())
532
self.assertEqualDiff("""\
533
------------------------------------------------------------
535
committer: Lorem Ipsum <test@example.com>
544
------------------------------------------------------------
546
committer: Lorem Ipsum <test@example.com>
550
removed f1 and modified f2
555
------------------------------------------------------------
557
committer: Lorem Ipsum <test@example.com>
568
def test_trailing_newlines(self):
569
wt = self.make_branch_and_tree('.')
570
b = make_commits_with_trailing_newlines(wt)
571
sio = self.make_utf8_encoded_stringio()
572
lf = log.LongLogFormatter(to_file=sio)
574
self.assertEqualDiff("""\
575
------------------------------------------------------------
577
committer: Joe Foo <joe@foo.com>
579
timestamp: Mon 2005-11-21 09:32:56 -0600
581
single line with trailing newline
582
------------------------------------------------------------
584
author: Joe Bar <joe@bar.com>
585
committer: Joe Foo <joe@foo.com>
587
timestamp: Mon 2005-11-21 09:27:22 -0600
592
------------------------------------------------------------
594
committer: Joe Foo <joe@foo.com>
596
timestamp: Mon 2005-11-21 09:24:15 -0600
602
def test_author_in_log(self):
603
"""Log includes the author name if it's set in
604
the revision properties
606
wt = self.make_branch_and_tree('.')
608
self.build_tree(['a'])
610
b.nick = 'test_author_log'
611
wt.commit(message='add a',
612
timestamp=1132711707,
614
committer='Lorem Ipsum <test@example.com>',
615
author='John Doe <jdoe@example.com>')
617
formatter = log.LongLogFormatter(to_file=sio)
618
log.show_log(b, formatter)
619
self.assertEqualDiff('''\
620
------------------------------------------------------------
622
author: John Doe <jdoe@example.com>
623
committer: Lorem Ipsum <test@example.com>
624
branch nick: test_author_log
625
timestamp: Wed 2005-11-23 12:08:27 +1000
631
def test_properties_in_log(self):
632
"""Log includes the custom properties returned by the registered
635
wt = self.make_branch_and_tree('.')
637
self.build_tree(['a'])
639
b.nick = 'test_properties_in_log'
640
wt.commit(message='add a',
641
timestamp=1132711707,
643
committer='Lorem Ipsum <test@example.com>',
644
author='John Doe <jdoe@example.com>')
646
formatter = log.LongLogFormatter(to_file=sio)
648
def trivial_custom_prop_handler(revision):
649
return {'test_prop':'test_value'}
651
log.properties_handler_registry.register(
652
'trivial_custom_prop_handler',
653
trivial_custom_prop_handler)
654
log.show_log(b, formatter)
656
log.properties_handler_registry.remove(
657
'trivial_custom_prop_handler')
658
self.assertEqualDiff('''\
659
------------------------------------------------------------
661
test_prop: test_value
662
author: John Doe <jdoe@example.com>
663
committer: Lorem Ipsum <test@example.com>
664
branch nick: test_properties_in_log
665
timestamp: Wed 2005-11-23 12:08:27 +1000
671
def test_error_in_properties_handler(self):
672
"""Log includes the custom properties returned by the registered
675
wt = self.make_branch_and_tree('.')
677
self.build_tree(['a'])
679
b.nick = 'test_author_log'
680
wt.commit(message='add a',
681
timestamp=1132711707,
683
committer='Lorem Ipsum <test@example.com>',
684
author='John Doe <jdoe@example.com>',
685
revprops={'first_prop':'first_value'})
687
formatter = log.LongLogFormatter(to_file=sio)
689
def trivial_custom_prop_handler(revision):
690
raise StandardError("a test error")
692
log.properties_handler_registry.register(
693
'trivial_custom_prop_handler',
694
trivial_custom_prop_handler)
695
self.assertRaises(StandardError, log.show_log, b, formatter,)
697
log.properties_handler_registry.remove(
698
'trivial_custom_prop_handler')
700
def test_properties_handler_bad_argument(self):
701
wt = self.make_branch_and_tree('.')
703
self.build_tree(['a'])
705
b.nick = 'test_author_log'
706
wt.commit(message='add a',
707
timestamp=1132711707,
709
committer='Lorem Ipsum <test@example.com>',
710
author='John Doe <jdoe@example.com>',
711
revprops={'a_prop':'test_value'})
713
formatter = log.LongLogFormatter(to_file=sio)
715
def bad_argument_prop_handler(revision):
716
return {'custom_prop_name':revision.properties['a_prop']}
718
log.properties_handler_registry.register(
719
'bad_argument_prop_handler',
720
bad_argument_prop_handler)
722
self.assertRaises(AttributeError, formatter.show_properties,
725
revision = b.repository.get_revision(b.last_revision())
726
formatter.show_properties(revision, '')
727
self.assertEqualDiff('''custom_prop_name: test_value\n''',
730
log.properties_handler_registry.remove(
731
'bad_argument_prop_handler')
734
class TestLineLogFormatter(tests.TestCaseWithTransport):
736
def test_line_log(self):
737
"""Line log should show revno
741
wt = self.make_branch_and_tree('.')
743
self.build_tree(['a'])
745
b.nick = 'test-line-log'
746
wt.commit(message='add a',
747
timestamp=1132711707,
749
committer='Line-Log-Formatter Tester <test@line.log>')
750
logfile = file('out.tmp', 'w+')
751
formatter = log.LineLogFormatter(to_file=logfile)
752
log.show_log(b, formatter)
755
log_contents = logfile.read()
756
self.assertEqualDiff('1: Line-Log-Formatte... 2005-11-23 add a\n',
759
def test_trailing_newlines(self):
760
wt = self.make_branch_and_tree('.')
761
b = make_commits_with_trailing_newlines(wt)
762
sio = self.make_utf8_encoded_stringio()
763
lf = log.LineLogFormatter(to_file=sio)
765
self.assertEqualDiff("""\
766
3: Joe Foo 2005-11-21 single line with trailing newline
767
2: Joe Bar 2005-11-21 multiline
768
1: Joe Foo 2005-11-21 simple log message
772
def _prepare_tree_with_merges(self, with_tags=False):
773
wt = self.make_branch_and_memory_tree('.')
775
self.addCleanup(wt.unlock)
777
wt.commit('rev-1', rev_id='rev-1',
778
timestamp=1132586655, timezone=36000,
779
committer='Joe Foo <joe@foo.com>')
780
wt.commit('rev-merged', rev_id='rev-2a',
781
timestamp=1132586700, timezone=36000,
782
committer='Joe Foo <joe@foo.com>')
783
wt.set_parent_ids(['rev-1', 'rev-2a'])
784
wt.branch.set_last_revision_info(1, 'rev-1')
785
wt.commit('rev-2', rev_id='rev-2b',
786
timestamp=1132586800, timezone=36000,
787
committer='Joe Foo <joe@foo.com>')
790
branch.tags.set_tag('v0.2', 'rev-2b')
791
wt.commit('rev-3', rev_id='rev-3',
792
timestamp=1132586900, timezone=36000,
793
committer='Jane Foo <jane@foo.com>')
794
branch.tags.set_tag('v1.0rc1', 'rev-3')
795
branch.tags.set_tag('v1.0', 'rev-3')
798
def test_line_log_single_merge_revision(self):
799
wt = self._prepare_tree_with_merges()
800
logfile = self.make_utf8_encoded_stringio()
801
formatter = log.LineLogFormatter(to_file=logfile)
802
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
804
rev = revspec.in_history(wtb)
805
log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
806
self.assertEqualDiff("""\
807
1.1.1: Joe Foo 2005-11-22 rev-merged
811
def test_line_log_with_tags(self):
812
wt = self._prepare_tree_with_merges(with_tags=True)
813
logfile = self.make_utf8_encoded_stringio()
814
formatter = log.LineLogFormatter(to_file=logfile)
815
log.show_log(wt.branch, formatter)
816
self.assertEqualDiff("""\
817
3: Jane Foo 2005-11-22 {v1.0, v1.0rc1} rev-3
818
2: Joe Foo 2005-11-22 {v0.2} rev-2
819
1: Joe Foo 2005-11-22 rev-1
824
class TestGetViewRevisions(tests.TestCaseWithTransport):
826
def make_tree_with_commits(self):
827
"""Create a tree with well-known revision ids"""
828
wt = self.make_branch_and_tree('tree1')
829
wt.commit('commit one', rev_id='1')
830
wt.commit('commit two', rev_id='2')
831
wt.commit('commit three', rev_id='3')
832
mainline_revs = [None, '1', '2', '3']
833
rev_nos = {'1': 1, '2': 2, '3': 3}
834
return mainline_revs, rev_nos, wt
836
def make_tree_with_merges(self):
837
"""Create a tree with well-known revision ids and a merge"""
838
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
839
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
840
tree2.commit('four-a', rev_id='4a')
841
wt.merge_from_branch(tree2.branch)
842
wt.commit('four-b', rev_id='4b')
843
mainline_revs.append('4b')
846
return mainline_revs, rev_nos, wt
848
def make_tree_with_many_merges(self):
849
"""Create a tree with well-known revision ids"""
850
wt = self.make_branch_and_tree('tree1')
851
self.build_tree_contents([('tree1/f', '1\n')])
852
wt.add(['f'], ['f-id'])
853
wt.commit('commit one', rev_id='1')
854
wt.commit('commit two', rev_id='2')
856
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
857
self.build_tree_contents([('tree3/f', '1\n2\n3a\n')])
858
tree3.commit('commit three a', rev_id='3a')
860
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
861
tree2.merge_from_branch(tree3.branch)
862
tree2.commit('commit three b', rev_id='3b')
864
wt.merge_from_branch(tree2.branch)
865
wt.commit('commit three c', rev_id='3c')
866
tree2.commit('four-a', rev_id='4a')
868
wt.merge_from_branch(tree2.branch)
869
wt.commit('four-b', rev_id='4b')
871
mainline_revs = [None, '1', '2', '3c', '4b']
872
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
873
full_rev_nos_for_reference = {
876
'3a': '2.1.1', #first commit tree 3
877
'3b': '2.2.1', # first commit tree 2
878
'3c': '3', #merges 3b to main
879
'4a': '2.2.2', # second commit tree 2
880
'4b': '4', # merges 4a to main
882
return mainline_revs, rev_nos, wt
884
def test_get_view_revisions_forward(self):
885
"""Test the get_view_revisions method"""
886
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
888
self.addCleanup(wt.unlock)
889
revisions = list(log.get_view_revisions(
890
mainline_revs, rev_nos, wt.branch, 'forward'))
891
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
893
revisions2 = list(log.get_view_revisions(
894
mainline_revs, rev_nos, wt.branch, 'forward',
895
include_merges=False))
896
self.assertEqual(revisions, revisions2)
898
def test_get_view_revisions_reverse(self):
899
"""Test the get_view_revisions with reverse"""
900
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
902
self.addCleanup(wt.unlock)
903
revisions = list(log.get_view_revisions(
904
mainline_revs, rev_nos, wt.branch, 'reverse'))
905
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
907
revisions2 = list(log.get_view_revisions(
908
mainline_revs, rev_nos, wt.branch, 'reverse',
909
include_merges=False))
910
self.assertEqual(revisions, revisions2)
912
def test_get_view_revisions_merge(self):
913
"""Test get_view_revisions when there are merges"""
914
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
916
self.addCleanup(wt.unlock)
917
revisions = list(log.get_view_revisions(
918
mainline_revs, rev_nos, wt.branch, 'forward'))
919
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
920
('4b', '4', 0), ('4a', '3.1.1', 1)],
922
revisions = list(log.get_view_revisions(
923
mainline_revs, rev_nos, wt.branch, 'forward',
924
include_merges=False))
925
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
929
def test_get_view_revisions_merge_reverse(self):
930
"""Test get_view_revisions in reverse when there are merges"""
931
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
933
self.addCleanup(wt.unlock)
934
revisions = list(log.get_view_revisions(
935
mainline_revs, rev_nos, wt.branch, 'reverse'))
936
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
937
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
939
revisions = list(log.get_view_revisions(
940
mainline_revs, rev_nos, wt.branch, 'reverse',
941
include_merges=False))
942
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
946
def test_get_view_revisions_merge2(self):
947
"""Test get_view_revisions when there are merges"""
948
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
950
self.addCleanup(wt.unlock)
951
revisions = list(log.get_view_revisions(
952
mainline_revs, rev_nos, wt.branch, 'forward'))
953
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
954
('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
956
self.assertEqual(expected, revisions)
957
revisions = list(log.get_view_revisions(
958
mainline_revs, rev_nos, wt.branch, 'forward',
959
include_merges=False))
960
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
965
def test_file_id_for_range(self):
966
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
968
self.addCleanup(wt.unlock)
970
def rev_from_rev_id(revid, branch):
971
revspec = revisionspec.RevisionSpec.from_string('revid:%s' % revid)
972
return revspec.in_history(branch)
974
def view_revs(start_rev, end_rev, file_id, direction):
975
revs = log.calculate_view_revisions(
977
start_rev, # start_revision
978
end_rev, # end_revision
979
direction, # direction
980
file_id, # specific_fileid
981
True, # generate_merge_revisions
982
True, # allow_single_merge_revision
986
rev_3a = rev_from_rev_id('3a', wt.branch)
987
rev_4b = rev_from_rev_id('4b', wt.branch)
988
self.assertEqual([('3c', '3', 0), ('3a', '2.1.1', 1)],
989
view_revs(rev_3a, rev_4b, 'f-id', 'reverse'))
990
# Note that the depth is 0 for 3a because depths are normalized, but
991
# there is still a bug somewhere... most probably in
992
# _filter_revision_range and/or get_view_revisions still around a bad
993
# use of reverse_by_depth
994
self.assertEqual([('3a', '2.1.1', 0)],
995
view_revs(rev_3a, rev_4b, 'f-id', 'forward'))
998
class TestGetRevisionsTouchingFileID(tests.TestCaseWithTransport):
1000
def create_tree_with_single_merge(self):
1001
"""Create a branch with a moderate layout.
1003
The revision graph looks like:
1011
In this graph, A introduced files f1 and f2 and f3.
1012
B modifies f1 and f3, and C modifies f2 and f3.
1013
D merges the changes from B and C and resolves the conflict for f3.
1015
# TODO: jam 20070218 This seems like it could really be done
1016
# with make_branch_and_memory_tree() if we could just
1017
# create the content of those files.
1018
# TODO: jam 20070218 Another alternative is that we would really
1019
# like to only create this tree 1 time for all tests that
1020
# use it. Since 'log' only uses the tree in a readonly
1021
# fashion, it seems a shame to regenerate an identical
1022
# tree for each test.
1023
tree = self.make_branch_and_tree('tree')
1025
self.addCleanup(tree.unlock)
1027
self.build_tree_contents([('tree/f1', 'A\n'),
1031
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
1032
tree.commit('A', rev_id='A')
1034
self.build_tree_contents([('tree/f2', 'A\nC\n'),
1035
('tree/f3', 'A\nC\n'),
1037
tree.commit('C', rev_id='C')
1038
# Revert back to A to build the other history.
1039
tree.set_last_revision('A')
1040
tree.branch.set_last_revision_info(1, 'A')
1041
self.build_tree_contents([('tree/f1', 'A\nB\n'),
1043
('tree/f3', 'A\nB\n'),
1045
tree.commit('B', rev_id='B')
1046
tree.set_parent_ids(['B', 'C'])
1047
self.build_tree_contents([('tree/f1', 'A\nB\n'),
1048
('tree/f2', 'A\nC\n'),
1049
('tree/f3', 'A\nB\nC\n'),
1051
tree.commit('D', rev_id='D')
1053
# Switch to a read lock for this tree.
1054
# We still have an addCleanup(tree.unlock) pending
1059
def check_delta(self, delta, **kw):
1060
"""Check the filenames touched by a delta are as expected.
1062
Caller only have to pass in the list of files for each part, all
1063
unspecified parts are considered empty (and checked as such).
1065
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
1066
# By default we expect an empty list
1067
expected = kw.get(n, [])
1068
# strip out only the path components
1069
got = [x[0] for x in getattr(delta, n)]
1070
self.assertEqual(expected, got)
1072
def test_tree_with_single_merge(self):
1073
"""Make sure the tree layout is correct."""
1074
tree = self.create_tree_with_single_merge()
1075
rev_A_tree = tree.branch.repository.revision_tree('A')
1076
rev_B_tree = tree.branch.repository.revision_tree('B')
1077
rev_C_tree = tree.branch.repository.revision_tree('C')
1078
rev_D_tree = tree.branch.repository.revision_tree('D')
1080
self.check_delta(rev_B_tree.changes_from(rev_A_tree),
1081
modified=['f1', 'f3'])
1083
self.check_delta(rev_C_tree.changes_from(rev_A_tree),
1084
modified=['f2', 'f3'])
1086
self.check_delta(rev_D_tree.changes_from(rev_B_tree),
1087
modified=['f2', 'f3'])
1089
self.check_delta(rev_D_tree.changes_from(rev_C_tree),
1090
modified=['f1', 'f3'])
1092
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
1093
"""Ensure _filter_revisions_touching_file_id returns the right values.
1095
Get the return value from _filter_revisions_touching_file_id and make
1096
sure they are correct.
1098
# The api for _filter_revisions_touching_file_id is a little crazy.
1099
# So we do the setup here.
1100
mainline = tree.branch.revision_history()
1101
mainline.insert(0, None)
1102
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
1103
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
1105
actual_revs = log._filter_revisions_touching_file_id(
1108
list(view_revs_iter))
1109
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
1111
def test_file_id_f1(self):
1112
tree = self.create_tree_with_single_merge()
1113
# f1 should be marked as modified by revisions A and B
1114
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
1116
def test_file_id_f2(self):
1117
tree = self.create_tree_with_single_merge()
1118
# f2 should be marked as modified by revisions A, C, and D
1119
# because D merged the changes from C.
1120
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1122
def test_file_id_f3(self):
1123
tree = self.create_tree_with_single_merge()
1124
# f3 should be marked as modified by revisions A, B, C, and D
1125
self.assertAllRevisionsForFileID(tree, 'f3-id', ['D', 'C', 'B', 'A'])
1127
def test_file_id_with_ghosts(self):
1128
# This is testing bug #209948, where having a ghost would cause
1129
# _filter_revisions_touching_file_id() to fail.
1130
tree = self.create_tree_with_single_merge()
1131
# We need to add a revision, so switch back to a write-locked tree
1132
# (still a single addCleanup(tree.unlock) pending).
1135
first_parent = tree.last_revision()
1136
tree.set_parent_ids([first_parent, 'ghost-revision-id'])
1137
self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
1138
tree.commit('commit with a ghost', rev_id='XX')
1139
self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
1140
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1143
class TestShowChangedRevisions(tests.TestCaseWithTransport):
1145
def test_show_changed_revisions_verbose(self):
1146
tree = self.make_branch_and_tree('tree_a')
1147
self.build_tree(['tree_a/foo'])
1149
tree.commit('bar', rev_id='bar-id')
1150
s = self.make_utf8_encoded_stringio()
1151
log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
1152
self.assertContainsRe(s.getvalue(), 'bar')
1153
self.assertNotContainsRe(s.getvalue(), 'foo')
1156
class TestLogFormatter(tests.TestCase):
1158
def test_short_committer(self):
1159
rev = revision.Revision('a-id')
1160
rev.committer = 'John Doe <jdoe@example.com>'
1161
lf = log.LogFormatter(None)
1162
self.assertEqual('John Doe', lf.short_committer(rev))
1163
rev.committer = 'John Smith <jsmith@example.com>'
1164
self.assertEqual('John Smith', lf.short_committer(rev))
1165
rev.committer = 'John Smith'
1166
self.assertEqual('John Smith', lf.short_committer(rev))
1167
rev.committer = 'jsmith@example.com'
1168
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1169
rev.committer = '<jsmith@example.com>'
1170
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1171
rev.committer = 'John Smith jsmith@example.com'
1172
self.assertEqual('John Smith', lf.short_committer(rev))
1174
def test_short_author(self):
1175
rev = revision.Revision('a-id')
1176
rev.committer = 'John Doe <jdoe@example.com>'
1177
lf = log.LogFormatter(None)
1178
self.assertEqual('John Doe', lf.short_author(rev))
1179
rev.properties['author'] = 'John Smith <jsmith@example.com>'
1180
self.assertEqual('John Smith', lf.short_author(rev))
1181
rev.properties['author'] = 'John Smith'
1182
self.assertEqual('John Smith', lf.short_author(rev))
1183
rev.properties['author'] = 'jsmith@example.com'
1184
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1185
rev.properties['author'] = '<jsmith@example.com>'
1186
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1187
rev.properties['author'] = 'John Smith jsmith@example.com'
1188
self.assertEqual('John Smith', lf.short_author(rev))
1191
class TestReverseByDepth(tests.TestCase):
1192
"""Test reverse_by_depth behavior.
1194
This is used to present revisions in forward (oldest first) order in a nice
1197
The tests use lighter revision description to ease reading.
1200
def assertReversed(self, forward, backward):
1201
# Transform the descriptions to suit the API: tests use (revno, depth),
1202
# while the API expects (revid, revno, depth)
1203
def complete_revisions(l):
1204
"""Transform the description to suit the API.
1206
Tests use (revno, depth) whil the API expects (revid, revno, depth).
1207
Since the revid is arbitrary, we just duplicate revno
1209
return [ (r, r, d) for r, d in l]
1210
forward = complete_revisions(forward)
1211
backward= complete_revisions(backward)
1212
self.assertEqual(forward, log.reverse_by_depth(backward))
1215
def test_mainline_revisions(self):
1216
self.assertReversed([( '1', 0), ('2', 0)],
1217
[('2', 0), ('1', 0)])
1219
def test_merged_revisions(self):
1220
self.assertReversed([('1', 0), ('2', 0), ('2.2', 1), ('2.1', 1),],
1221
[('2', 0), ('2.1', 1), ('2.2', 1), ('1', 0),])
1222
def test_shifted_merged_revisions(self):
1223
"""Test irregular layout.
1225
Requesting revisions touching a file can produce "holes" in the depths.
1227
self.assertReversed([('1', 0), ('2', 0), ('1.1', 2), ('1.2', 2),],
1228
[('2', 0), ('1.2', 2), ('1.1', 2), ('1', 0),])
1230
def test_merged_without_child_revisions(self):
1231
"""Test irregular layout.
1233
Revision ranges can produce "holes" in the depths.
1235
# When a revision of higher depth doesn't follow one of lower depth, we
1236
# assume a lower depth one is virtually there
1237
self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1238
[('4', 4), ('3', 3), ('2', 2), ('1', 2),])
1239
# So we get the same order after reversing below even if the original
1240
# revisions are not in the same order.
1241
self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1242
[('3', 3), ('4', 4), ('2', 2), ('1', 2),])
1245
class TestHistoryChange(tests.TestCaseWithTransport):
1247
def setup_a_tree(self):
1248
tree = self.make_branch_and_tree('tree')
1250
self.addCleanup(tree.unlock)
1251
tree.commit('1a', rev_id='1a')
1252
tree.commit('2a', rev_id='2a')
1253
tree.commit('3a', rev_id='3a')
1256
def setup_ab_tree(self):
1257
tree = self.setup_a_tree()
1258
tree.set_last_revision('1a')
1259
tree.branch.set_last_revision_info(1, '1a')
1260
tree.commit('2b', rev_id='2b')
1261
tree.commit('3b', rev_id='3b')
1264
def setup_ac_tree(self):
1265
tree = self.setup_a_tree()
1266
tree.set_last_revision(revision.NULL_REVISION)
1267
tree.branch.set_last_revision_info(0, revision.NULL_REVISION)
1268
tree.commit('1c', rev_id='1c')
1269
tree.commit('2c', rev_id='2c')
1270
tree.commit('3c', rev_id='3c')
1273
def test_all_new(self):
1274
tree = self.setup_ab_tree()
1275
old, new = log.get_history_change('1a', '3a', tree.branch.repository)
1276
self.assertEqual([], old)
1277
self.assertEqual(['2a', '3a'], new)
1279
def test_all_old(self):
1280
tree = self.setup_ab_tree()
1281
old, new = log.get_history_change('3a', '1a', tree.branch.repository)
1282
self.assertEqual([], new)
1283
self.assertEqual(['2a', '3a'], old)
1285
def test_null_old(self):
1286
tree = self.setup_ab_tree()
1287
old, new = log.get_history_change(revision.NULL_REVISION,
1288
'3a', tree.branch.repository)
1289
self.assertEqual([], old)
1290
self.assertEqual(['1a', '2a', '3a'], new)
1292
def test_null_new(self):
1293
tree = self.setup_ab_tree()
1294
old, new = log.get_history_change('3a', revision.NULL_REVISION,
1295
tree.branch.repository)
1296
self.assertEqual([], new)
1297
self.assertEqual(['1a', '2a', '3a'], old)
1299
def test_diverged(self):
1300
tree = self.setup_ab_tree()
1301
old, new = log.get_history_change('3a', '3b', tree.branch.repository)
1302
self.assertEqual(old, ['2a', '3a'])
1303
self.assertEqual(new, ['2b', '3b'])
1305
def test_unrelated(self):
1306
tree = self.setup_ac_tree()
1307
old, new = log.get_history_change('3a', '3c', tree.branch.repository)
1308
self.assertEqual(old, ['1a', '2a', '3a'])
1309
self.assertEqual(new, ['1c', '2c', '3c'])
1311
def test_show_branch_change(self):
1312
tree = self.setup_ab_tree()
1314
log.show_branch_change(tree.branch, s, 3, '3a')
1315
self.assertContainsRe(s.getvalue(),
1316
'[*]{60}\nRemoved Revisions:\n(.|\n)*2a(.|\n)*3a(.|\n)*'
1317
'[*]{60}\n\nAdded Revisions:\n(.|\n)*2b(.|\n)*3b')
1319
def test_show_branch_change_no_change(self):
1320
tree = self.setup_ab_tree()
1322
log.show_branch_change(tree.branch, s, 3, '3b')
1323
self.assertEqual(s.getvalue(),
1324
'Nothing seems to have changed\n')
1326
def test_show_branch_change_no_old(self):
1327
tree = self.setup_ab_tree()
1329
log.show_branch_change(tree.branch, s, 2, '2b')
1330
self.assertContainsRe(s.getvalue(), 'Added Revisions:')
1331
self.assertNotContainsRe(s.getvalue(), 'Removed Revisions:')
1333
def test_show_branch_change_no_new(self):
1334
tree = self.setup_ab_tree()
1335
tree.branch.set_last_revision_info(2, '2b')
1337
log.show_branch_change(tree.branch, s, 3, '3b')
1338
self.assertContainsRe(s.getvalue(), 'Removed Revisions:')
1339
self.assertNotContainsRe(s.getvalue(), 'Added Revisions:')