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
20
from bzrlib import log, registry
21
from bzrlib.tests import TestCase, TestCaseWithTransport
22
from bzrlib.log import (show_log,
29
from bzrlib.branch import Branch
30
from bzrlib.errors import (
32
InvalidRevisionNumber,
34
from bzrlib.revision import Revision
35
from bzrlib.revisionspec import (
41
class TestCaseWithoutPropsHandler(TestCaseWithTransport):
44
super(TestCaseWithoutPropsHandler, self).setUp()
45
# keep a reference to the "current" custom prop. handler registry
46
self.properties_handler_registry = \
47
log.properties_handler_registry
48
# clean up the registry in log
49
log.properties_handler_registry = registry.Registry()
52
super(TestCaseWithoutPropsHandler, self)._cleanup()
53
# restore the custom properties handler registry
54
log.properties_handler_registry = \
55
self.properties_handler_registry
58
class LogCatcher(LogFormatter):
59
"""Pull log messages into list rather than displaying them.
61
For ease of testing we save log messages here rather than actually
62
formatting them, so that we can precisely check the result without
63
being too dependent on the exact formatting.
65
We should also test the LogFormatter.
71
super(LogCatcher, self).__init__(to_file=None)
74
def log_revision(self, revision):
75
self.logs.append(revision)
78
class TestShowLog(TestCaseWithTransport):
80
def checkDelta(self, delta, **kw):
81
"""Check the filenames touched by a delta are as expected."""
82
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
83
expected = kw.get(n, [])
84
# strip out only the path components
85
got = [x[0] for x in getattr(delta, n)]
86
self.assertEquals(expected, got)
88
def test_cur_revno(self):
89
wt = self.make_branch_and_tree('.')
93
wt.commit('empty commit')
94
show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
95
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
96
start_revision=2, end_revision=1)
97
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
98
start_revision=1, end_revision=2)
99
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
100
start_revision=0, end_revision=2)
101
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
102
start_revision=1, end_revision=0)
103
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
104
start_revision=-1, end_revision=1)
105
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
106
start_revision=1, end_revision=-1)
108
def test_simple_log(self):
109
eq = self.assertEquals
111
wt = self.make_branch_and_tree('.')
119
wt.commit('empty commit')
121
show_log(b, lf, verbose=True)
123
eq(lf.logs[0].revno, '1')
124
eq(lf.logs[0].rev.message, 'empty commit')
126
self.log('log delta: %r' % d)
129
self.build_tree(['hello'])
131
wt.commit('add one file',
132
committer=u'\u013d\xf3r\xe9m \xcdp\u0161\xfam '
133
u'<test@example.com>')
135
lf = self.make_utf8_encoded_stringio()
136
# log using regular thing
137
show_log(b, LongLogFormatter(lf))
139
for l in lf.readlines():
142
# get log as data structure
144
show_log(b, lf, verbose=True)
146
self.log('log entries:')
147
for logentry in lf.logs:
148
self.log('%4s %s' % (logentry.revno, logentry.rev.message))
150
# first one is most recent
151
logentry = lf.logs[0]
152
eq(logentry.revno, '2')
153
eq(logentry.rev.message, 'add one file')
155
self.log('log 2 delta: %r' % d)
156
self.checkDelta(d, added=['hello'])
158
# commit a log message with control characters
159
msg = "All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
160
self.log("original commit message: %r", msg)
163
show_log(b, lf, verbose=True)
164
committed_msg = lf.logs[0].rev.message
165
self.log("escaped commit message: %r", committed_msg)
166
self.assert_(msg != committed_msg)
167
self.assert_(len(committed_msg) > len(msg))
169
# Check that log message with only XML-valid characters isn't
170
# escaped. As ElementTree apparently does some kind of
171
# newline conversion, neither LF (\x0A) nor CR (\x0D) are
172
# included in the test commit message, even though they are
173
# valid XML 1.0 characters.
174
msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
175
self.log("original commit message: %r", msg)
178
show_log(b, lf, verbose=True)
179
committed_msg = lf.logs[0].rev.message
180
self.log("escaped commit message: %r", committed_msg)
181
self.assert_(msg == committed_msg)
183
def test_deltas_in_merge_revisions(self):
184
"""Check deltas created for both mainline and merge revisions"""
185
eq = self.assertEquals
186
wt = self.make_branch_and_tree('parent')
187
self.build_tree(['parent/file1', 'parent/file2', 'parent/file3'])
190
wt.commit(message='add file1 and file2')
191
self.run_bzr('branch parent child')
192
os.unlink('child/file1')
193
file('child/file2', 'wb').write('hello\n')
194
self.run_bzr(['commit', '-m', 'remove file1 and modify file2',
197
self.run_bzr('merge ../child')
198
wt.commit('merge child branch')
202
lf.supports_merge_revisions = True
203
show_log(b, lf, verbose=True)
205
logentry = lf.logs[0]
206
eq(logentry.revno, '2')
207
eq(logentry.rev.message, 'merge child branch')
209
self.checkDelta(d, removed=['file1'], modified=['file2'])
210
logentry = lf.logs[1]
211
eq(logentry.revno, '1.1.1')
212
eq(logentry.rev.message, 'remove file1 and modify file2')
214
self.checkDelta(d, removed=['file1'], modified=['file2'])
215
logentry = lf.logs[2]
216
eq(logentry.revno, '1')
217
eq(logentry.rev.message, 'add file1 and file2')
219
self.checkDelta(d, added=['file1', 'file2'])
221
def test_merges_nonsupporting_formatter(self):
222
"""Tests that show_log will raise if the formatter doesn't
223
support merge revisions."""
224
wt = self.make_branch_and_memory_tree('.')
228
wt.commit('rev-1', rev_id='rev-1',
229
timestamp=1132586655, timezone=36000,
230
committer='Joe Foo <joe@foo.com>')
231
wt.commit('rev-merged', rev_id='rev-2a',
232
timestamp=1132586700, timezone=36000,
233
committer='Joe Foo <joe@foo.com>')
234
wt.set_parent_ids(['rev-1', 'rev-2a'])
235
wt.branch.set_last_revision_info(1, 'rev-1')
236
wt.commit('rev-2', rev_id='rev-2b',
237
timestamp=1132586800, timezone=36000,
238
committer='Joe Foo <joe@foo.com>')
239
logfile = self.make_utf8_encoded_stringio()
240
formatter = ShortLogFormatter(to_file=logfile)
243
revspec = RevisionSpec.from_string('1.1.1')
244
rev = revspec.in_history(wtb)
245
self.assertRaises(BzrCommandError, show_log, wtb, lf,
246
start_revision=rev, end_revision=rev)
251
def make_commits_with_trailing_newlines(wt):
252
"""Helper method for LogFormatter tests"""
255
open('a', 'wb').write('hello moto\n')
257
wt.commit('simple log message', rev_id='a1',
258
timestamp=1132586655.459960938, timezone=-6*3600,
259
committer='Joe Foo <joe@foo.com>')
260
open('b', 'wb').write('goodbye\n')
262
wt.commit('multiline\nlog\nmessage\n', rev_id='a2',
263
timestamp=1132586842.411175966, timezone=-6*3600,
264
committer='Joe Foo <joe@foo.com>',
265
author='Joe Bar <joe@bar.com>')
267
open('c', 'wb').write('just another manic monday\n')
269
wt.commit('single line with trailing newline\n', rev_id='a3',
270
timestamp=1132587176.835228920, timezone=-6*3600,
271
committer = 'Joe Foo <joe@foo.com>')
275
def normalize_log(log):
276
"""Replaces the variable lines of logs with fixed lines"""
277
author = 'author: Dolor Sit <test@example.com>'
278
committer = 'committer: Lorem Ipsum <test@example.com>'
279
lines = log.splitlines(True)
280
for idx,line in enumerate(lines):
281
stripped_line = line.lstrip()
282
indent = ' ' * (len(line) - len(stripped_line))
283
if stripped_line.startswith('author:'):
284
lines[idx] = indent + author + '\n'
285
elif stripped_line.startswith('committer:'):
286
lines[idx] = indent + committer + '\n'
287
elif stripped_line.startswith('timestamp:'):
288
lines[idx] = indent + 'timestamp: Just now\n'
289
return ''.join(lines)
292
class TestShortLogFormatter(TestCaseWithTransport):
294
def test_trailing_newlines(self):
295
wt = self.make_branch_and_tree('.')
296
b = make_commits_with_trailing_newlines(wt)
297
sio = self.make_utf8_encoded_stringio()
298
lf = ShortLogFormatter(to_file=sio)
300
self.assertEqualDiff(sio.getvalue(), """\
301
3 Joe Foo\t2005-11-21
302
single line with trailing newline
304
2 Joe Bar\t2005-11-21
309
1 Joe Foo\t2005-11-21
314
def test_short_log_with_merges(self):
315
wt = self.make_branch_and_memory_tree('.')
319
wt.commit('rev-1', rev_id='rev-1',
320
timestamp=1132586655, timezone=36000,
321
committer='Joe Foo <joe@foo.com>')
322
wt.commit('rev-merged', rev_id='rev-2a',
323
timestamp=1132586700, timezone=36000,
324
committer='Joe Foo <joe@foo.com>')
325
wt.set_parent_ids(['rev-1', 'rev-2a'])
326
wt.branch.set_last_revision_info(1, 'rev-1')
327
wt.commit('rev-2', rev_id='rev-2b',
328
timestamp=1132586800, timezone=36000,
329
committer='Joe Foo <joe@foo.com>')
330
logfile = self.make_utf8_encoded_stringio()
331
formatter = ShortLogFormatter(to_file=logfile)
332
show_log(wt.branch, formatter)
333
self.assertEqualDiff(logfile.getvalue(), """\
334
2 Joe Foo\t2005-11-22 [merge]
337
1 Joe Foo\t2005-11-22
344
def test_short_log_single_merge_revision(self):
345
wt = self.make_branch_and_memory_tree('.')
349
wt.commit('rev-1', rev_id='rev-1',
350
timestamp=1132586655, timezone=36000,
351
committer='Joe Foo <joe@foo.com>')
352
wt.commit('rev-merged', rev_id='rev-2a',
353
timestamp=1132586700, timezone=36000,
354
committer='Joe Foo <joe@foo.com>')
355
wt.set_parent_ids(['rev-1', 'rev-2a'])
356
wt.branch.set_last_revision_info(1, 'rev-1')
357
wt.commit('rev-2', rev_id='rev-2b',
358
timestamp=1132586800, timezone=36000,
359
committer='Joe Foo <joe@foo.com>')
360
logfile = self.make_utf8_encoded_stringio()
361
formatter = ShortLogFormatter(to_file=logfile)
362
revspec = RevisionSpec.from_string('1.1.1')
364
rev = revspec.in_history(wtb)
365
show_log(wtb, formatter, start_revision=rev, end_revision=rev)
366
self.assertEqualDiff(logfile.getvalue(), """\
367
1.1.1 Joe Foo\t2005-11-22
375
class TestLongLogFormatter(TestCaseWithoutPropsHandler):
377
def test_verbose_log(self):
378
"""Verbose log includes changed files
382
wt = self.make_branch_and_tree('.')
384
self.build_tree(['a'])
386
# XXX: why does a longer nick show up?
387
b.nick = 'test_verbose_log'
388
wt.commit(message='add a',
389
timestamp=1132711707,
391
committer='Lorem Ipsum <test@example.com>')
392
logfile = file('out.tmp', 'w+')
393
formatter = LongLogFormatter(to_file=logfile)
394
show_log(b, formatter, verbose=True)
397
log_contents = logfile.read()
398
self.assertEqualDiff(log_contents, '''\
399
------------------------------------------------------------
401
committer: Lorem Ipsum <test@example.com>
402
branch nick: test_verbose_log
403
timestamp: Wed 2005-11-23 12:08:27 +1000
410
def test_merges_are_indented_by_level(self):
411
wt = self.make_branch_and_tree('parent')
412
wt.commit('first post')
413
self.run_bzr('branch parent child')
414
self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
415
self.run_bzr('branch child smallerchild')
416
self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
419
self.run_bzr('merge ../smallerchild')
420
self.run_bzr(['commit', '-m', 'merge branch 2'])
421
os.chdir('../parent')
422
self.run_bzr('merge ../child')
423
wt.commit('merge branch 1')
425
sio = self.make_utf8_encoded_stringio()
426
lf = LongLogFormatter(to_file=sio)
427
show_log(b, lf, verbose=True)
428
log = normalize_log(sio.getvalue())
429
self.assertEqualDiff(log, """\
430
------------------------------------------------------------
432
committer: Lorem Ipsum <test@example.com>
437
------------------------------------------------------------
439
committer: Lorem Ipsum <test@example.com>
444
------------------------------------------------------------
446
committer: Lorem Ipsum <test@example.com>
447
branch nick: smallerchild
451
------------------------------------------------------------
453
committer: Lorem Ipsum <test@example.com>
458
------------------------------------------------------------
460
committer: Lorem Ipsum <test@example.com>
467
def test_verbose_merge_revisions_contain_deltas(self):
468
wt = self.make_branch_and_tree('parent')
469
self.build_tree(['parent/f1', 'parent/f2'])
471
wt.commit('first post')
472
self.run_bzr('branch parent child')
473
os.unlink('child/f1')
474
file('child/f2', 'wb').write('hello\n')
475
self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
478
self.run_bzr('merge ../child')
479
wt.commit('merge branch 1')
481
sio = self.make_utf8_encoded_stringio()
482
lf = LongLogFormatter(to_file=sio)
483
show_log(b, lf, verbose=True)
484
log = normalize_log(sio.getvalue())
485
self.assertEqualDiff(log, """\
486
------------------------------------------------------------
488
committer: Lorem Ipsum <test@example.com>
497
------------------------------------------------------------
499
committer: Lorem Ipsum <test@example.com>
503
removed f1 and modified f2
508
------------------------------------------------------------
510
committer: Lorem Ipsum <test@example.com>
520
def test_trailing_newlines(self):
521
wt = self.make_branch_and_tree('.')
522
b = make_commits_with_trailing_newlines(wt)
523
sio = self.make_utf8_encoded_stringio()
524
lf = LongLogFormatter(to_file=sio)
526
self.assertEqualDiff(sio.getvalue(), """\
527
------------------------------------------------------------
529
committer: Joe Foo <joe@foo.com>
531
timestamp: Mon 2005-11-21 09:32:56 -0600
533
single line with trailing newline
534
------------------------------------------------------------
536
author: Joe Bar <joe@bar.com>
537
committer: Joe Foo <joe@foo.com>
539
timestamp: Mon 2005-11-21 09:27:22 -0600
544
------------------------------------------------------------
546
committer: Joe Foo <joe@foo.com>
548
timestamp: Mon 2005-11-21 09:24:15 -0600
553
def test_author_in_log(self):
554
"""Log includes the author name if it's set in
555
the revision properties
557
wt = self.make_branch_and_tree('.')
559
self.build_tree(['a'])
561
b.nick = 'test_author_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 = LongLogFormatter(to_file=sio)
569
show_log(b, formatter)
570
self.assertEqualDiff(sio.getvalue(), '''\
571
------------------------------------------------------------
573
author: John Doe <jdoe@example.com>
574
committer: Lorem Ipsum <test@example.com>
575
branch nick: test_author_log
576
timestamp: Wed 2005-11-23 12:08:27 +1000
581
def test_properties_in_log(self):
582
"""Log includes the custom properties returned by the registered
585
wt = self.make_branch_and_tree('.')
587
self.build_tree(['a'])
589
b.nick = 'test_properties_in_log'
590
wt.commit(message='add a',
591
timestamp=1132711707,
593
committer='Lorem Ipsum <test@example.com>',
594
author='John Doe <jdoe@example.com>')
596
formatter = LongLogFormatter(to_file=sio)
598
def trivial_custom_prop_handler(revision):
599
return {'test_prop':'test_value'}
601
log.properties_handler_registry.register(
602
'trivial_custom_prop_handler',
603
trivial_custom_prop_handler)
604
show_log(b, formatter)
606
log.properties_handler_registry.remove(
607
'trivial_custom_prop_handler')
608
self.assertEqualDiff(sio.getvalue(), '''\
609
------------------------------------------------------------
611
test_prop: test_value
612
author: John Doe <jdoe@example.com>
613
committer: Lorem Ipsum <test@example.com>
614
branch nick: test_properties_in_log
615
timestamp: Wed 2005-11-23 12:08:27 +1000
620
def test_error_in_properties_handler(self):
621
"""Log includes the custom properties returned by the registered
624
wt = self.make_branch_and_tree('.')
626
self.build_tree(['a'])
628
b.nick = 'test_author_log'
629
wt.commit(message='add a',
630
timestamp=1132711707,
632
committer='Lorem Ipsum <test@example.com>',
633
author='John Doe <jdoe@example.com>',
634
revprops={'first_prop':'first_value'})
636
formatter = LongLogFormatter(to_file=sio)
638
def trivial_custom_prop_handler(revision):
639
raise StandardError("a test error")
641
log.properties_handler_registry.register(
642
'trivial_custom_prop_handler',
643
trivial_custom_prop_handler)
644
self.assertRaises(StandardError, show_log, b, formatter,)
646
log.properties_handler_registry.remove(
647
'trivial_custom_prop_handler')
649
def test_properties_handler_bad_argument(self):
650
wt = self.make_branch_and_tree('.')
652
self.build_tree(['a'])
654
b.nick = 'test_author_log'
655
wt.commit(message='add a',
656
timestamp=1132711707,
658
committer='Lorem Ipsum <test@example.com>',
659
author='John Doe <jdoe@example.com>',
660
revprops={'a_prop':'test_value'})
662
formatter = LongLogFormatter(to_file=sio)
664
def bad_argument_prop_handler(revision):
665
return {'custom_prop_name':revision.properties['a_prop']}
667
log.properties_handler_registry.register(
668
'bad_argument_prop_handler',
669
bad_argument_prop_handler)
671
self.assertRaises(AttributeError, formatter.show_properties,
674
revision = b.repository.get_revision(b.last_revision())
675
formatter.show_properties(revision, '')
676
self.assertEqualDiff(sio.getvalue(),
677
'''custom_prop_name: test_value\n''')
679
log.properties_handler_registry.remove(
680
'bad_argument_prop_handler')
683
class TestLineLogFormatter(TestCaseWithTransport):
685
def test_line_log(self):
686
"""Line log should show revno
690
wt = self.make_branch_and_tree('.')
692
self.build_tree(['a'])
694
b.nick = 'test-line-log'
695
wt.commit(message='add a',
696
timestamp=1132711707,
698
committer='Line-Log-Formatter Tester <test@line.log>')
699
logfile = file('out.tmp', 'w+')
700
formatter = LineLogFormatter(to_file=logfile)
701
show_log(b, formatter)
704
log_contents = logfile.read()
705
self.assertEqualDiff(log_contents,
706
'1: Line-Log-Formatte... 2005-11-23 add a\n')
708
def test_trailing_newlines(self):
709
wt = self.make_branch_and_tree('.')
710
b = make_commits_with_trailing_newlines(wt)
711
sio = self.make_utf8_encoded_stringio()
712
lf = LineLogFormatter(to_file=sio)
714
self.assertEqualDiff(sio.getvalue(), """\
715
3: Joe Foo 2005-11-21 single line with trailing newline
716
2: Joe Bar 2005-11-21 multiline
717
1: Joe Foo 2005-11-21 simple log message
720
def test_line_log_single_merge_revision(self):
721
wt = self.make_branch_and_memory_tree('.')
725
wt.commit('rev-1', rev_id='rev-1',
726
timestamp=1132586655, timezone=36000,
727
committer='Joe Foo <joe@foo.com>')
728
wt.commit('rev-merged', rev_id='rev-2a',
729
timestamp=1132586700, timezone=36000,
730
committer='Joe Foo <joe@foo.com>')
731
wt.set_parent_ids(['rev-1', 'rev-2a'])
732
wt.branch.set_last_revision_info(1, 'rev-1')
733
wt.commit('rev-2', rev_id='rev-2b',
734
timestamp=1132586800, timezone=36000,
735
committer='Joe Foo <joe@foo.com>')
736
logfile = self.make_utf8_encoded_stringio()
737
formatter = LineLogFormatter(to_file=logfile)
738
revspec = RevisionSpec.from_string('1.1.1')
740
rev = revspec.in_history(wtb)
741
show_log(wtb, formatter, start_revision=rev, end_revision=rev)
742
self.assertEqualDiff(logfile.getvalue(), """\
743
1.1.1: Joe Foo 2005-11-22 rev-merged
750
class TestGetViewRevisions(TestCaseWithTransport):
752
def make_tree_with_commits(self):
753
"""Create a tree with well-known revision ids"""
754
wt = self.make_branch_and_tree('tree1')
755
wt.commit('commit one', rev_id='1')
756
wt.commit('commit two', rev_id='2')
757
wt.commit('commit three', rev_id='3')
758
mainline_revs = [None, '1', '2', '3']
759
rev_nos = {'1': 1, '2': 2, '3': 3}
760
return mainline_revs, rev_nos, wt
762
def make_tree_with_merges(self):
763
"""Create a tree with well-known revision ids and a merge"""
764
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
765
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
766
tree2.commit('four-a', rev_id='4a')
767
wt.merge_from_branch(tree2.branch)
768
wt.commit('four-b', rev_id='4b')
769
mainline_revs.append('4b')
772
return mainline_revs, rev_nos, wt
774
def make_tree_with_many_merges(self):
775
"""Create a tree with well-known revision ids"""
776
wt = self.make_branch_and_tree('tree1')
777
wt.commit('commit one', rev_id='1')
778
wt.commit('commit two', rev_id='2')
779
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
780
tree3.commit('commit three a', rev_id='3a')
781
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
782
tree2.merge_from_branch(tree3.branch)
783
tree2.commit('commit three b', rev_id='3b')
784
wt.merge_from_branch(tree2.branch)
785
wt.commit('commit three c', rev_id='3c')
786
tree2.commit('four-a', rev_id='4a')
787
wt.merge_from_branch(tree2.branch)
788
wt.commit('four-b', rev_id='4b')
789
mainline_revs = [None, '1', '2', '3c', '4b']
790
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
791
full_rev_nos_for_reference = {
794
'3a': '2.1.1', #first commit tree 3
795
'3b': '2.2.1', # first commit tree 2
796
'3c': '3', #merges 3b to main
797
'4a': '2.2.2', # second commit tree 2
798
'4b': '4', # merges 4a to main
800
return mainline_revs, rev_nos, wt
802
def test_get_view_revisions_forward(self):
803
"""Test the get_view_revisions method"""
804
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
806
self.addCleanup(wt.unlock)
807
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
809
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
811
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
812
'forward', include_merges=False))
813
self.assertEqual(revisions, revisions2)
815
def test_get_view_revisions_reverse(self):
816
"""Test the get_view_revisions with reverse"""
817
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
819
self.addCleanup(wt.unlock)
820
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
822
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
824
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
825
'reverse', include_merges=False))
826
self.assertEqual(revisions, revisions2)
828
def test_get_view_revisions_merge(self):
829
"""Test get_view_revisions when there are merges"""
830
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
832
self.addCleanup(wt.unlock)
833
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
835
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
836
('4b', '4', 0), ('4a', '3.1.1', 1)],
838
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
839
'forward', include_merges=False))
840
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
844
def test_get_view_revisions_merge_reverse(self):
845
"""Test get_view_revisions in reverse when there are merges"""
846
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
848
self.addCleanup(wt.unlock)
849
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
851
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
852
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
854
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
855
'reverse', include_merges=False))
856
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
860
def test_get_view_revisions_merge2(self):
861
"""Test get_view_revisions when there are merges"""
862
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
864
self.addCleanup(wt.unlock)
865
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
867
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
868
('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
870
self.assertEqual(expected, revisions)
871
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
872
'forward', include_merges=False))
873
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
878
class TestGetRevisionsTouchingFileID(TestCaseWithTransport):
880
def create_tree_with_single_merge(self):
881
"""Create a branch with a moderate layout.
883
The revision graph looks like:
891
In this graph, A introduced files f1 and f2 and f3.
892
B modifies f1 and f3, and C modifies f2 and f3.
893
D merges the changes from B and C and resolves the conflict for f3.
895
# TODO: jam 20070218 This seems like it could really be done
896
# with make_branch_and_memory_tree() if we could just
897
# create the content of those files.
898
# TODO: jam 20070218 Another alternative is that we would really
899
# like to only create this tree 1 time for all tests that
900
# use it. Since 'log' only uses the tree in a readonly
901
# fashion, it seems a shame to regenerate an identical
902
# tree for each test.
903
tree = self.make_branch_and_tree('tree')
905
self.addCleanup(tree.unlock)
907
self.build_tree_contents([('tree/f1', 'A\n'),
911
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
912
tree.commit('A', rev_id='A')
914
self.build_tree_contents([('tree/f2', 'A\nC\n'),
915
('tree/f3', 'A\nC\n'),
917
tree.commit('C', rev_id='C')
918
# Revert back to A to build the other history.
919
tree.set_last_revision('A')
920
tree.branch.set_last_revision_info(1, 'A')
921
self.build_tree_contents([('tree/f1', 'A\nB\n'),
923
('tree/f3', 'A\nB\n'),
925
tree.commit('B', rev_id='B')
926
tree.set_parent_ids(['B', 'C'])
927
self.build_tree_contents([('tree/f1', 'A\nB\n'),
928
('tree/f2', 'A\nC\n'),
929
('tree/f3', 'A\nB\nC\n'),
931
tree.commit('D', rev_id='D')
933
# Switch to a read lock for this tree.
934
# We still have addCleanup(unlock)
939
def test_tree_with_single_merge(self):
940
"""Make sure the tree layout is correct."""
941
tree = self.create_tree_with_single_merge()
942
rev_A_tree = tree.branch.repository.revision_tree('A')
943
rev_B_tree = tree.branch.repository.revision_tree('B')
945
f1_changed = (u'f1', 'f1-id', 'file', True, False)
946
f2_changed = (u'f2', 'f2-id', 'file', True, False)
947
f3_changed = (u'f3', 'f3-id', 'file', True, False)
949
delta = rev_B_tree.changes_from(rev_A_tree)
950
self.assertEqual([f1_changed, f3_changed], delta.modified)
951
self.assertEqual([], delta.renamed)
952
self.assertEqual([], delta.added)
953
self.assertEqual([], delta.removed)
955
rev_C_tree = tree.branch.repository.revision_tree('C')
956
delta = rev_C_tree.changes_from(rev_A_tree)
957
self.assertEqual([f2_changed, f3_changed], delta.modified)
958
self.assertEqual([], delta.renamed)
959
self.assertEqual([], delta.added)
960
self.assertEqual([], delta.removed)
962
rev_D_tree = tree.branch.repository.revision_tree('D')
963
delta = rev_D_tree.changes_from(rev_B_tree)
964
self.assertEqual([f2_changed, f3_changed], delta.modified)
965
self.assertEqual([], delta.renamed)
966
self.assertEqual([], delta.added)
967
self.assertEqual([], delta.removed)
969
delta = rev_D_tree.changes_from(rev_C_tree)
970
self.assertEqual([f1_changed, f3_changed], delta.modified)
971
self.assertEqual([], delta.renamed)
972
self.assertEqual([], delta.added)
973
self.assertEqual([], delta.removed)
975
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
976
"""Make sure _filter_revisions_touching_file_id returns the right values.
978
Get the return value from _filter_revisions_touching_file_id and make
979
sure they are correct.
981
# The api for _get_revisions_touching_file_id is a little crazy,
982
# So we do the setup here.
983
mainline = tree.branch.revision_history()
984
mainline.insert(0, None)
985
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
986
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
988
actual_revs = log._filter_revisions_touching_file_id(
991
list(view_revs_iter),
993
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
995
def test_file_id_f1(self):
996
tree = self.create_tree_with_single_merge()
997
# f1 should be marked as modified by revisions A and B
998
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
1000
def test_file_id_f2(self):
1001
tree = self.create_tree_with_single_merge()
1002
# f2 should be marked as modified by revisions A, C, and D
1003
# because D merged the changes from C.
1004
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1006
def test_file_id_f3(self):
1007
tree = self.create_tree_with_single_merge()
1008
# f3 should be marked as modified by revisions A, B, C, and D
1009
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1011
def test_file_id_with_ghosts(self):
1012
# This is testing bug #209948, where having a ghost would cause
1013
# _filter_revisions_touching_file_id() to fail.
1014
tree = self.create_tree_with_single_merge()
1015
# We need to add a revision, so switch back to a write-locked tree
1018
first_parent = tree.last_revision()
1019
tree.set_parent_ids([first_parent, 'ghost-revision-id'])
1020
self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
1021
tree.commit('commit with a ghost', rev_id='XX')
1022
self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
1023
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1026
class TestShowChangedRevisions(TestCaseWithTransport):
1028
def test_show_changed_revisions_verbose(self):
1029
tree = self.make_branch_and_tree('tree_a')
1030
self.build_tree(['tree_a/foo'])
1032
tree.commit('bar', rev_id='bar-id')
1033
s = self.make_utf8_encoded_stringio()
1034
log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
1035
self.assertContainsRe(s.getvalue(), 'bar')
1036
self.assertNotContainsRe(s.getvalue(), 'foo')
1039
class TestLogFormatter(TestCase):
1041
def test_short_committer(self):
1042
rev = Revision('a-id')
1043
rev.committer = 'John Doe <jdoe@example.com>'
1044
lf = LogFormatter(None)
1045
self.assertEqual('John Doe', lf.short_committer(rev))
1046
rev.committer = 'John Smith <jsmith@example.com>'
1047
self.assertEqual('John Smith', lf.short_committer(rev))
1048
rev.committer = 'John Smith'
1049
self.assertEqual('John Smith', lf.short_committer(rev))
1050
rev.committer = 'jsmith@example.com'
1051
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1052
rev.committer = '<jsmith@example.com>'
1053
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1054
rev.committer = 'John Smith jsmith@example.com'
1055
self.assertEqual('John Smith', lf.short_committer(rev))
1057
def test_short_author(self):
1058
rev = Revision('a-id')
1059
rev.committer = 'John Doe <jdoe@example.com>'
1060
lf = LogFormatter(None)
1061
self.assertEqual('John Doe', lf.short_author(rev))
1062
rev.properties['author'] = 'John Smith <jsmith@example.com>'
1063
self.assertEqual('John Smith', lf.short_author(rev))
1064
rev.properties['author'] = 'John Smith'
1065
self.assertEqual('John Smith', lf.short_author(rev))
1066
rev.properties['author'] = 'jsmith@example.com'
1067
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1068
rev.properties['author'] = '<jsmith@example.com>'
1069
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1070
rev.properties['author'] = 'John Smith jsmith@example.com'
1071
self.assertEqual('John Smith', lf.short_author(rev))