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
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 LogCatcher(LogFormatter):
42
"""Pull log messages into list rather than displaying them.
44
For ease of testing we save log messages here rather than actually
45
formatting them, so that we can precisely check the result without
46
being too dependent on the exact formatting.
48
We should also test the LogFormatter.
54
super(LogCatcher, self).__init__(to_file=None)
57
def log_revision(self, revision):
58
self.logs.append(revision)
61
class TestShowLog(TestCaseWithTransport):
63
def checkDelta(self, delta, **kw):
64
"""Check the filenames touched by a delta are as expected."""
65
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
66
expected = kw.get(n, [])
67
# strip out only the path components
68
got = [x[0] for x in getattr(delta, n)]
69
self.assertEquals(expected, got)
71
def test_cur_revno(self):
72
wt = self.make_branch_and_tree('.')
76
wt.commit('empty commit')
77
show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
78
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
79
start_revision=2, end_revision=1)
80
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
81
start_revision=1, end_revision=2)
82
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
83
start_revision=0, end_revision=2)
84
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
85
start_revision=1, end_revision=0)
86
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
87
start_revision=-1, end_revision=1)
88
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
89
start_revision=1, end_revision=-1)
91
def test_simple_log(self):
92
eq = self.assertEquals
94
wt = self.make_branch_and_tree('.')
102
wt.commit('empty commit')
104
show_log(b, lf, verbose=True)
106
eq(lf.logs[0].revno, '1')
107
eq(lf.logs[0].rev.message, 'empty commit')
109
self.log('log delta: %r' % d)
112
self.build_tree(['hello'])
114
wt.commit('add one file',
115
committer=u'\u013d\xf3r\xe9m \xcdp\u0161\xfam '
116
u'<test@example.com>')
118
lf = self.make_utf8_encoded_stringio()
119
# log using regular thing
120
show_log(b, LongLogFormatter(lf))
122
for l in lf.readlines():
125
# get log as data structure
127
show_log(b, lf, verbose=True)
129
self.log('log entries:')
130
for logentry in lf.logs:
131
self.log('%4s %s' % (logentry.revno, logentry.rev.message))
133
# first one is most recent
134
logentry = lf.logs[0]
135
eq(logentry.revno, '2')
136
eq(logentry.rev.message, 'add one file')
138
self.log('log 2 delta: %r' % d)
139
self.checkDelta(d, added=['hello'])
141
# commit a log message with control characters
142
msg = "All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
143
self.log("original commit message: %r", msg)
146
show_log(b, lf, verbose=True)
147
committed_msg = lf.logs[0].rev.message
148
self.log("escaped commit message: %r", committed_msg)
149
self.assert_(msg != committed_msg)
150
self.assert_(len(committed_msg) > len(msg))
152
# Check that log message with only XML-valid characters isn't
153
# escaped. As ElementTree apparently does some kind of
154
# newline conversion, neither LF (\x0A) nor CR (\x0D) are
155
# included in the test commit message, even though they are
156
# valid XML 1.0 characters.
157
msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
158
self.log("original commit message: %r", msg)
161
show_log(b, lf, verbose=True)
162
committed_msg = lf.logs[0].rev.message
163
self.log("escaped commit message: %r", committed_msg)
164
self.assert_(msg == committed_msg)
166
def test_deltas_in_merge_revisions(self):
167
"""Check deltas created for both mainline and merge revisions"""
168
eq = self.assertEquals
169
wt = self.make_branch_and_tree('parent')
170
self.build_tree(['parent/file1', 'parent/file2', 'parent/file3'])
173
wt.commit(message='add file1 and file2')
174
self.run_bzr('branch parent child')
175
os.unlink('child/file1')
176
file('child/file2', 'wb').write('hello\n')
177
self.run_bzr(['commit', '-m', 'remove file1 and modify file2',
180
self.run_bzr('merge ../child')
181
wt.commit('merge child branch')
185
lf.supports_merge_revisions = True
186
show_log(b, lf, verbose=True)
188
logentry = lf.logs[0]
189
eq(logentry.revno, '2')
190
eq(logentry.rev.message, 'merge child branch')
192
self.checkDelta(d, removed=['file1'], modified=['file2'])
193
logentry = lf.logs[1]
194
eq(logentry.revno, '1.1.1')
195
eq(logentry.rev.message, 'remove file1 and modify file2')
197
self.checkDelta(d, removed=['file1'], modified=['file2'])
198
logentry = lf.logs[2]
199
eq(logentry.revno, '1')
200
eq(logentry.rev.message, 'add file1 and file2')
202
self.checkDelta(d, added=['file1', 'file2'])
204
def test_merges_nonsupporting_formatter(self):
205
"""Tests that show_log will raise if the formatter doesn't
206
support merge revisions."""
207
wt = self.make_branch_and_memory_tree('.')
211
wt.commit('rev-1', rev_id='rev-1',
212
timestamp=1132586655, timezone=36000,
213
committer='Joe Foo <joe@foo.com>')
214
wt.commit('rev-merged', rev_id='rev-2a',
215
timestamp=1132586700, timezone=36000,
216
committer='Joe Foo <joe@foo.com>')
217
wt.set_parent_ids(['rev-1', 'rev-2a'])
218
wt.branch.set_last_revision_info(1, 'rev-1')
219
wt.commit('rev-2', rev_id='rev-2b',
220
timestamp=1132586800, timezone=36000,
221
committer='Joe Foo <joe@foo.com>')
222
logfile = self.make_utf8_encoded_stringio()
223
formatter = ShortLogFormatter(to_file=logfile)
226
revspec = RevisionSpec.from_string('1.1.1')
227
rev = revspec.in_history(wtb)
228
self.assertRaises(BzrCommandError, show_log, wtb, lf,
229
start_revision=rev, end_revision=rev)
234
def make_commits_with_trailing_newlines(wt):
235
"""Helper method for LogFormatter tests"""
238
open('a', 'wb').write('hello moto\n')
240
wt.commit('simple log message', rev_id='a1',
241
timestamp=1132586655.459960938, timezone=-6*3600,
242
committer='Joe Foo <joe@foo.com>')
243
open('b', 'wb').write('goodbye\n')
245
wt.commit('multiline\nlog\nmessage\n', rev_id='a2',
246
timestamp=1132586842.411175966, timezone=-6*3600,
247
committer='Joe Foo <joe@foo.com>',
248
author='Joe Bar <joe@bar.com>')
250
open('c', 'wb').write('just another manic monday\n')
252
wt.commit('single line with trailing newline\n', rev_id='a3',
253
timestamp=1132587176.835228920, timezone=-6*3600,
254
committer = 'Joe Foo <joe@foo.com>')
258
def normalize_log(log):
259
"""Replaces the variable lines of logs with fixed lines"""
260
author = 'author: Dolor Sit <test@example.com>'
261
committer = 'committer: Lorem Ipsum <test@example.com>'
262
lines = log.splitlines(True)
263
for idx,line in enumerate(lines):
264
stripped_line = line.lstrip()
265
indent = ' ' * (len(line) - len(stripped_line))
266
if stripped_line.startswith('author:'):
267
lines[idx] = indent + author + '\n'
268
elif stripped_line.startswith('committer:'):
269
lines[idx] = indent + committer + '\n'
270
elif stripped_line.startswith('timestamp:'):
271
lines[idx] = indent + 'timestamp: Just now\n'
272
return ''.join(lines)
275
class TestShortLogFormatter(TestCaseWithTransport):
277
def test_trailing_newlines(self):
278
wt = self.make_branch_and_tree('.')
279
b = make_commits_with_trailing_newlines(wt)
280
sio = self.make_utf8_encoded_stringio()
281
lf = ShortLogFormatter(to_file=sio)
283
self.assertEqualDiff(sio.getvalue(), """\
284
3 Joe Foo\t2005-11-21
285
single line with trailing newline
287
2 Joe Bar\t2005-11-21
292
1 Joe Foo\t2005-11-21
297
def test_short_log_with_merges(self):
298
wt = self.make_branch_and_memory_tree('.')
302
wt.commit('rev-1', rev_id='rev-1',
303
timestamp=1132586655, timezone=36000,
304
committer='Joe Foo <joe@foo.com>')
305
wt.commit('rev-merged', rev_id='rev-2a',
306
timestamp=1132586700, timezone=36000,
307
committer='Joe Foo <joe@foo.com>')
308
wt.set_parent_ids(['rev-1', 'rev-2a'])
309
wt.branch.set_last_revision_info(1, 'rev-1')
310
wt.commit('rev-2', rev_id='rev-2b',
311
timestamp=1132586800, timezone=36000,
312
committer='Joe Foo <joe@foo.com>')
313
logfile = self.make_utf8_encoded_stringio()
314
formatter = ShortLogFormatter(to_file=logfile)
315
show_log(wt.branch, formatter)
316
self.assertEqualDiff(logfile.getvalue(), """\
317
2 Joe Foo\t2005-11-22 [merge]
320
1 Joe Foo\t2005-11-22
327
def test_short_log_single_merge_revision(self):
328
wt = self.make_branch_and_memory_tree('.')
332
wt.commit('rev-1', rev_id='rev-1',
333
timestamp=1132586655, timezone=36000,
334
committer='Joe Foo <joe@foo.com>')
335
wt.commit('rev-merged', rev_id='rev-2a',
336
timestamp=1132586700, timezone=36000,
337
committer='Joe Foo <joe@foo.com>')
338
wt.set_parent_ids(['rev-1', 'rev-2a'])
339
wt.branch.set_last_revision_info(1, 'rev-1')
340
wt.commit('rev-2', rev_id='rev-2b',
341
timestamp=1132586800, timezone=36000,
342
committer='Joe Foo <joe@foo.com>')
343
logfile = self.make_utf8_encoded_stringio()
344
formatter = ShortLogFormatter(to_file=logfile)
345
revspec = RevisionSpec.from_string('1.1.1')
347
rev = revspec.in_history(wtb)
348
show_log(wtb, formatter, start_revision=rev, end_revision=rev)
349
self.assertEqualDiff(logfile.getvalue(), """\
350
1.1.1 Joe Foo\t2005-11-22
358
class TestLongLogFormatter(TestCaseWithTransport):
360
def test_verbose_log(self):
361
"""Verbose log includes changed files
365
wt = self.make_branch_and_tree('.')
367
self.build_tree(['a'])
369
# XXX: why does a longer nick show up?
370
b.nick = 'test_verbose_log'
371
wt.commit(message='add a',
372
timestamp=1132711707,
374
committer='Lorem Ipsum <test@example.com>')
375
logfile = file('out.tmp', 'w+')
376
formatter = LongLogFormatter(to_file=logfile)
377
show_log(b, formatter, verbose=True)
380
log_contents = logfile.read()
381
self.assertEqualDiff(log_contents, '''\
382
------------------------------------------------------------
384
committer: Lorem Ipsum <test@example.com>
385
branch nick: test_verbose_log
386
timestamp: Wed 2005-11-23 12:08:27 +1000
393
def test_merges_are_indented_by_level(self):
394
wt = self.make_branch_and_tree('parent')
395
wt.commit('first post')
396
self.run_bzr('branch parent child')
397
self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
398
self.run_bzr('branch child smallerchild')
399
self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
402
self.run_bzr('merge ../smallerchild')
403
self.run_bzr(['commit', '-m', 'merge branch 2'])
404
os.chdir('../parent')
405
self.run_bzr('merge ../child')
406
wt.commit('merge branch 1')
408
sio = self.make_utf8_encoded_stringio()
409
lf = LongLogFormatter(to_file=sio)
410
show_log(b, lf, verbose=True)
411
log = normalize_log(sio.getvalue())
412
self.assertEqualDiff(log, """\
413
------------------------------------------------------------
415
committer: Lorem Ipsum <test@example.com>
420
------------------------------------------------------------
422
committer: Lorem Ipsum <test@example.com>
427
------------------------------------------------------------
429
committer: Lorem Ipsum <test@example.com>
430
branch nick: smallerchild
434
------------------------------------------------------------
436
committer: Lorem Ipsum <test@example.com>
441
------------------------------------------------------------
443
committer: Lorem Ipsum <test@example.com>
450
def test_verbose_merge_revisions_contain_deltas(self):
451
wt = self.make_branch_and_tree('parent')
452
self.build_tree(['parent/f1', 'parent/f2'])
454
wt.commit('first post')
455
self.run_bzr('branch parent child')
456
os.unlink('child/f1')
457
file('child/f2', 'wb').write('hello\n')
458
self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
461
self.run_bzr('merge ../child')
462
wt.commit('merge branch 1')
464
sio = self.make_utf8_encoded_stringio()
465
lf = LongLogFormatter(to_file=sio)
466
show_log(b, lf, verbose=True)
467
log = normalize_log(sio.getvalue())
468
self.assertEqualDiff(log, """\
469
------------------------------------------------------------
471
committer: Lorem Ipsum <test@example.com>
480
------------------------------------------------------------
482
committer: Lorem Ipsum <test@example.com>
486
removed f1 and modified f2
491
------------------------------------------------------------
493
committer: Lorem Ipsum <test@example.com>
503
def test_trailing_newlines(self):
504
wt = self.make_branch_and_tree('.')
505
b = make_commits_with_trailing_newlines(wt)
506
sio = self.make_utf8_encoded_stringio()
507
lf = LongLogFormatter(to_file=sio)
509
self.assertEqualDiff(sio.getvalue(), """\
510
------------------------------------------------------------
512
committer: Joe Foo <joe@foo.com>
514
timestamp: Mon 2005-11-21 09:32:56 -0600
516
single line with trailing newline
517
------------------------------------------------------------
519
author: Joe Bar <joe@bar.com>
520
committer: Joe Foo <joe@foo.com>
522
timestamp: Mon 2005-11-21 09:27:22 -0600
527
------------------------------------------------------------
529
committer: Joe Foo <joe@foo.com>
531
timestamp: Mon 2005-11-21 09:24:15 -0600
536
def test_author_in_log(self):
537
"""Log includes the author name if it's set in
538
the revision properties
540
wt = self.make_branch_and_tree('.')
542
self.build_tree(['a'])
544
b.nick = 'test_author_log'
545
wt.commit(message='add a',
546
timestamp=1132711707,
548
committer='Lorem Ipsum <test@example.com>',
549
author='John Doe <jdoe@example.com>')
551
formatter = LongLogFormatter(to_file=sio)
552
show_log(b, formatter)
553
self.assertEqualDiff(sio.getvalue(), '''\
554
------------------------------------------------------------
556
author: John Doe <jdoe@example.com>
557
committer: Lorem Ipsum <test@example.com>
558
branch nick: test_author_log
559
timestamp: Wed 2005-11-23 12:08:27 +1000
566
class TestLineLogFormatter(TestCaseWithTransport):
568
def test_line_log(self):
569
"""Line log should show revno
573
wt = self.make_branch_and_tree('.')
575
self.build_tree(['a'])
577
b.nick = 'test-line-log'
578
wt.commit(message='add a',
579
timestamp=1132711707,
581
committer='Line-Log-Formatter Tester <test@line.log>')
582
logfile = file('out.tmp', 'w+')
583
formatter = LineLogFormatter(to_file=logfile)
584
show_log(b, formatter)
587
log_contents = logfile.read()
588
self.assertEqualDiff(log_contents,
589
'1: Line-Log-Formatte... 2005-11-23 add a\n')
591
def test_trailing_newlines(self):
592
wt = self.make_branch_and_tree('.')
593
b = make_commits_with_trailing_newlines(wt)
594
sio = self.make_utf8_encoded_stringio()
595
lf = LineLogFormatter(to_file=sio)
597
self.assertEqualDiff(sio.getvalue(), """\
598
3: Joe Foo 2005-11-21 single line with trailing newline
599
2: Joe Bar 2005-11-21 multiline
600
1: Joe Foo 2005-11-21 simple log message
603
def test_line_log_single_merge_revision(self):
604
wt = self.make_branch_and_memory_tree('.')
608
wt.commit('rev-1', rev_id='rev-1',
609
timestamp=1132586655, timezone=36000,
610
committer='Joe Foo <joe@foo.com>')
611
wt.commit('rev-merged', rev_id='rev-2a',
612
timestamp=1132586700, timezone=36000,
613
committer='Joe Foo <joe@foo.com>')
614
wt.set_parent_ids(['rev-1', 'rev-2a'])
615
wt.branch.set_last_revision_info(1, 'rev-1')
616
wt.commit('rev-2', rev_id='rev-2b',
617
timestamp=1132586800, timezone=36000,
618
committer='Joe Foo <joe@foo.com>')
619
logfile = self.make_utf8_encoded_stringio()
620
formatter = LineLogFormatter(to_file=logfile)
621
revspec = RevisionSpec.from_string('1.1.1')
623
rev = revspec.in_history(wtb)
624
show_log(wtb, formatter, start_revision=rev, end_revision=rev)
625
self.assertEqualDiff(logfile.getvalue(), """\
626
1.1.1: Joe Foo 2005-11-22 rev-merged
633
class TestGetViewRevisions(TestCaseWithTransport):
635
def make_tree_with_commits(self):
636
"""Create a tree with well-known revision ids"""
637
wt = self.make_branch_and_tree('tree1')
638
wt.commit('commit one', rev_id='1')
639
wt.commit('commit two', rev_id='2')
640
wt.commit('commit three', rev_id='3')
641
mainline_revs = [None, '1', '2', '3']
642
rev_nos = {'1': 1, '2': 2, '3': 3}
643
return mainline_revs, rev_nos, wt
645
def make_tree_with_merges(self):
646
"""Create a tree with well-known revision ids and a merge"""
647
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
648
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
649
tree2.commit('four-a', rev_id='4a')
650
wt.merge_from_branch(tree2.branch)
651
wt.commit('four-b', rev_id='4b')
652
mainline_revs.append('4b')
655
return mainline_revs, rev_nos, wt
657
def make_tree_with_many_merges(self):
658
"""Create a tree with well-known revision ids"""
659
wt = self.make_branch_and_tree('tree1')
660
wt.commit('commit one', rev_id='1')
661
wt.commit('commit two', rev_id='2')
662
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
663
tree3.commit('commit three a', rev_id='3a')
664
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
665
tree2.merge_from_branch(tree3.branch)
666
tree2.commit('commit three b', rev_id='3b')
667
wt.merge_from_branch(tree2.branch)
668
wt.commit('commit three c', rev_id='3c')
669
tree2.commit('four-a', rev_id='4a')
670
wt.merge_from_branch(tree2.branch)
671
wt.commit('four-b', rev_id='4b')
672
mainline_revs = [None, '1', '2', '3c', '4b']
673
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
674
full_rev_nos_for_reference = {
677
'3a': '2.1.1', #first commit tree 3
678
'3b': '2.2.1', # first commit tree 2
679
'3c': '3', #merges 3b to main
680
'4a': '2.2.2', # second commit tree 2
681
'4b': '4', # merges 4a to main
683
return mainline_revs, rev_nos, wt
685
def test_get_view_revisions_forward(self):
686
"""Test the get_view_revisions method"""
687
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
689
self.addCleanup(wt.unlock)
690
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
692
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
694
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
695
'forward', include_merges=False))
696
self.assertEqual(revisions, revisions2)
698
def test_get_view_revisions_reverse(self):
699
"""Test the get_view_revisions with reverse"""
700
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
702
self.addCleanup(wt.unlock)
703
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
705
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
707
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
708
'reverse', include_merges=False))
709
self.assertEqual(revisions, revisions2)
711
def test_get_view_revisions_merge(self):
712
"""Test get_view_revisions when there are merges"""
713
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
715
self.addCleanup(wt.unlock)
716
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
718
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
719
('4b', '4', 0), ('4a', '3.1.1', 1)],
721
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
722
'forward', include_merges=False))
723
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
727
def test_get_view_revisions_merge_reverse(self):
728
"""Test get_view_revisions in reverse when there are merges"""
729
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
731
self.addCleanup(wt.unlock)
732
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
734
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
735
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
737
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
738
'reverse', include_merges=False))
739
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
743
def test_get_view_revisions_merge2(self):
744
"""Test get_view_revisions when there are merges"""
745
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
747
self.addCleanup(wt.unlock)
748
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
750
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
751
('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
753
self.assertEqual(expected, revisions)
754
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
755
'forward', include_merges=False))
756
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
761
class TestGetRevisionsTouchingFileID(TestCaseWithTransport):
763
def create_tree_with_single_merge(self):
764
"""Create a branch with a moderate layout.
766
The revision graph looks like:
774
In this graph, A introduced files f1 and f2 and f3.
775
B modifies f1 and f3, and C modifies f2 and f3.
776
D merges the changes from B and C and resolves the conflict for f3.
778
# TODO: jam 20070218 This seems like it could really be done
779
# with make_branch_and_memory_tree() if we could just
780
# create the content of those files.
781
# TODO: jam 20070218 Another alternative is that we would really
782
# like to only create this tree 1 time for all tests that
783
# use it. Since 'log' only uses the tree in a readonly
784
# fashion, it seems a shame to regenerate an identical
785
# tree for each test.
786
tree = self.make_branch_and_tree('tree')
788
self.addCleanup(tree.unlock)
790
self.build_tree_contents([('tree/f1', 'A\n'),
794
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
795
tree.commit('A', rev_id='A')
797
self.build_tree_contents([('tree/f2', 'A\nC\n'),
798
('tree/f3', 'A\nC\n'),
800
tree.commit('C', rev_id='C')
801
# Revert back to A to build the other history.
802
tree.set_last_revision('A')
803
tree.branch.set_last_revision_info(1, 'A')
804
self.build_tree_contents([('tree/f1', 'A\nB\n'),
806
('tree/f3', 'A\nB\n'),
808
tree.commit('B', rev_id='B')
809
tree.set_parent_ids(['B', 'C'])
810
self.build_tree_contents([('tree/f1', 'A\nB\n'),
811
('tree/f2', 'A\nC\n'),
812
('tree/f3', 'A\nB\nC\n'),
814
tree.commit('D', rev_id='D')
816
# Switch to a read lock for this tree.
817
# We still have addCleanup(unlock)
822
def test_tree_with_single_merge(self):
823
"""Make sure the tree layout is correct."""
824
tree = self.create_tree_with_single_merge()
825
rev_A_tree = tree.branch.repository.revision_tree('A')
826
rev_B_tree = tree.branch.repository.revision_tree('B')
828
f1_changed = (u'f1', 'f1-id', 'file', True, False)
829
f2_changed = (u'f2', 'f2-id', 'file', True, False)
830
f3_changed = (u'f3', 'f3-id', 'file', True, False)
832
delta = rev_B_tree.changes_from(rev_A_tree)
833
self.assertEqual([f1_changed, f3_changed], delta.modified)
834
self.assertEqual([], delta.renamed)
835
self.assertEqual([], delta.added)
836
self.assertEqual([], delta.removed)
838
rev_C_tree = tree.branch.repository.revision_tree('C')
839
delta = rev_C_tree.changes_from(rev_A_tree)
840
self.assertEqual([f2_changed, f3_changed], delta.modified)
841
self.assertEqual([], delta.renamed)
842
self.assertEqual([], delta.added)
843
self.assertEqual([], delta.removed)
845
rev_D_tree = tree.branch.repository.revision_tree('D')
846
delta = rev_D_tree.changes_from(rev_B_tree)
847
self.assertEqual([f2_changed, f3_changed], delta.modified)
848
self.assertEqual([], delta.renamed)
849
self.assertEqual([], delta.added)
850
self.assertEqual([], delta.removed)
852
delta = rev_D_tree.changes_from(rev_C_tree)
853
self.assertEqual([f1_changed, f3_changed], delta.modified)
854
self.assertEqual([], delta.renamed)
855
self.assertEqual([], delta.added)
856
self.assertEqual([], delta.removed)
858
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
859
"""Make sure _filter_revisions_touching_file_id returns the right values.
861
Get the return value from _filter_revisions_touching_file_id and make
862
sure they are correct.
864
# The api for _get_revisions_touching_file_id is a little crazy,
865
# So we do the setup here.
866
mainline = tree.branch.revision_history()
867
mainline.insert(0, None)
868
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
869
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
871
actual_revs = log._filter_revisions_touching_file_id(
875
list(view_revs_iter))
876
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
878
def test_file_id_f1(self):
879
tree = self.create_tree_with_single_merge()
880
# f1 should be marked as modified by revisions A and B
881
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
883
def test_file_id_f2(self):
884
tree = self.create_tree_with_single_merge()
885
# f2 should be marked as modified by revisions A, C, and D
886
# because D merged the changes from C.
887
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
889
def test_file_id_f3(self):
890
tree = self.create_tree_with_single_merge()
891
# f3 should be marked as modified by revisions A, B, C, and D
892
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
894
def test_file_id_with_ghosts(self):
895
# This is testing bug #209948, where having a ghost would cause
896
# _filter_revisions_touching_file_id() to fail.
897
tree = self.create_tree_with_single_merge()
898
# We need to add a revision, so switch back to a write-locked tree
901
first_parent = tree.last_revision()
902
tree.set_parent_ids([first_parent, 'ghost-revision-id'])
903
self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
904
tree.commit('commit with a ghost', rev_id='XX')
905
self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
906
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
909
class TestShowChangedRevisions(TestCaseWithTransport):
911
def test_show_changed_revisions_verbose(self):
912
tree = self.make_branch_and_tree('tree_a')
913
self.build_tree(['tree_a/foo'])
915
tree.commit('bar', rev_id='bar-id')
916
s = self.make_utf8_encoded_stringio()
917
log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
918
self.assertContainsRe(s.getvalue(), 'bar')
919
self.assertNotContainsRe(s.getvalue(), 'foo')
922
class TestLogFormatter(TestCase):
924
def test_short_committer(self):
925
rev = Revision('a-id')
926
rev.committer = 'John Doe <jdoe@example.com>'
927
lf = LogFormatter(None)
928
self.assertEqual('John Doe', lf.short_committer(rev))
929
rev.committer = 'John Smith <jsmith@example.com>'
930
self.assertEqual('John Smith', lf.short_committer(rev))
931
rev.committer = 'John Smith'
932
self.assertEqual('John Smith', lf.short_committer(rev))
933
rev.committer = 'jsmith@example.com'
934
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
935
rev.committer = '<jsmith@example.com>'
936
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
937
rev.committer = 'John Smith jsmith@example.com'
938
self.assertEqual('John Smith', lf.short_committer(rev))
940
def test_short_author(self):
941
rev = Revision('a-id')
942
rev.committer = 'John Doe <jdoe@example.com>'
943
lf = LogFormatter(None)
944
self.assertEqual('John Doe', lf.short_author(rev))
945
rev.properties['author'] = 'John Smith <jsmith@example.com>'
946
self.assertEqual('John Smith', lf.short_author(rev))
947
rev.properties['author'] = 'John Smith'
948
self.assertEqual('John Smith', lf.short_author(rev))
949
rev.properties['author'] = 'jsmith@example.com'
950
self.assertEqual('jsmith@example.com', lf.short_author(rev))
951
rev.properties['author'] = '<jsmith@example.com>'
952
self.assertEqual('jsmith@example.com', lf.short_author(rev))
953
rev.properties['author'] = 'John Smith jsmith@example.com'
954
self.assertEqual('John Smith', lf.short_author(rev))