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 TestCaseWithTransport
22
from bzrlib.log import (show_log,
29
from bzrlib.branch import Branch
30
from bzrlib.errors import InvalidRevisionNumber
33
class LogCatcher(LogFormatter):
34
"""Pull log messages into list rather than displaying them.
36
For ease of testing we save log messages here rather than actually
37
formatting them, so that we can precisely check the result without
38
being too dependent on the exact formatting.
40
We should also test the LogFormatter.
46
super(LogCatcher, self).__init__(to_file=None)
49
def log_revision(self, revision):
50
self.logs.append(revision)
53
class TestShowLog(TestCaseWithTransport):
55
def checkDelta(self, delta, **kw):
56
"""Check the filenames touched by a delta are as expected."""
57
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
58
expected = kw.get(n, [])
59
# strip out only the path components
60
got = [x[0] for x in getattr(delta, n)]
61
self.assertEquals(expected, got)
63
def test_cur_revno(self):
64
wt = self.make_branch_and_tree('.')
68
wt.commit('empty commit')
69
show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
70
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
71
start_revision=2, end_revision=1)
72
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
73
start_revision=1, end_revision=2)
74
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
75
start_revision=0, end_revision=2)
76
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
77
start_revision=1, end_revision=0)
78
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
79
start_revision=-1, end_revision=1)
80
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
81
start_revision=1, end_revision=-1)
83
def test_simple_log(self):
84
eq = self.assertEquals
86
wt = self.make_branch_and_tree('.')
94
wt.commit('empty commit')
96
show_log(b, lf, verbose=True)
98
eq(lf.logs[0].revno, '1')
99
eq(lf.logs[0].rev.message, 'empty commit')
101
self.log('log delta: %r' % d)
104
self.build_tree(['hello'])
106
wt.commit('add one file')
109
# log using regular thing
110
show_log(b, LongLogFormatter(lf))
112
for l in lf.readlines():
115
# get log as data structure
117
show_log(b, lf, verbose=True)
119
self.log('log entries:')
120
for logentry in lf.logs:
121
self.log('%4s %s' % (logentry.revno, logentry.rev.message))
123
# first one is most recent
124
logentry = lf.logs[0]
125
eq(logentry.revno, '2')
126
eq(logentry.rev.message, 'add one file')
128
self.log('log 2 delta: %r' % d)
129
self.checkDelta(d, added=['hello'])
131
# commit a log message with control characters
132
msg = "All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
133
self.log("original commit message: %r", msg)
136
show_log(b, lf, verbose=True)
137
committed_msg = lf.logs[0].rev.message
138
self.log("escaped commit message: %r", committed_msg)
139
self.assert_(msg != committed_msg)
140
self.assert_(len(committed_msg) > len(msg))
142
# Check that log message with only XML-valid characters isn't
143
# escaped. As ElementTree apparently does some kind of
144
# newline conversion, neither LF (\x0A) nor CR (\x0D) are
145
# included in the test commit message, even though they are
146
# valid XML 1.0 characters.
147
msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
148
self.log("original commit message: %r", msg)
151
show_log(b, lf, verbose=True)
152
committed_msg = lf.logs[0].rev.message
153
self.log("escaped commit message: %r", committed_msg)
154
self.assert_(msg == committed_msg)
156
def test_deltas_in_merge_revisions(self):
157
"""Check deltas created for both mainline and merge revisions"""
158
eq = self.assertEquals
159
wt = self.make_branch_and_tree('parent')
160
self.build_tree(['parent/file1', 'parent/file2', 'parent/file3'])
163
wt.commit(message='add file1 and file2')
164
self.run_bzr('branch', 'parent', 'child')
165
os.unlink('child/file1')
166
print >> file('child/file2', 'wb'), 'hello'
167
self.run_bzr('commit', '-m', 'remove file1 and modify file2', 'child')
169
self.run_bzr('merge', '../child')
170
wt.commit('merge child branch')
174
lf.supports_merge_revisions = True
175
show_log(b, lf, verbose=True)
177
logentry = lf.logs[0]
178
eq(logentry.revno, '2')
179
eq(logentry.rev.message, 'merge child branch')
181
self.checkDelta(d, removed=['file1'], modified=['file2'])
182
logentry = lf.logs[1]
183
eq(logentry.revno, '1.1.1')
184
eq(logentry.rev.message, 'remove file1 and modify file2')
186
self.checkDelta(d, removed=['file1'], modified=['file2'])
187
logentry = lf.logs[2]
188
eq(logentry.revno, '1')
189
eq(logentry.rev.message, 'add file1 and file2')
191
self.checkDelta(d, added=['file1', 'file2'])
194
def make_commits_with_trailing_newlines(wt):
195
"""Helper method for LogFormatter tests"""
198
open('a', 'wb').write('hello moto\n')
200
wt.commit('simple log message', rev_id='a1'
201
, timestamp=1132586655.459960938, timezone=-6*3600
202
, committer='Joe Foo <joe@foo.com>')
203
open('b', 'wb').write('goodbye\n')
205
wt.commit('multiline\nlog\nmessage\n', rev_id='a2'
206
, timestamp=1132586842.411175966, timezone=-6*3600
207
, committer='Joe Foo <joe@foo.com>')
209
open('c', 'wb').write('just another manic monday\n')
211
wt.commit('single line with trailing newline\n', rev_id='a3'
212
, timestamp=1132587176.835228920, timezone=-6*3600
213
, committer = 'Joe Foo <joe@foo.com>')
217
class TestShortLogFormatter(TestCaseWithTransport):
219
def test_trailing_newlines(self):
220
wt = self.make_branch_and_tree('.')
221
b = make_commits_with_trailing_newlines(wt)
223
lf = ShortLogFormatter(to_file=sio)
225
self.assertEquals(sio.getvalue(), """\
226
3 Joe Foo\t2005-11-21
227
single line with trailing newline
229
2 Joe Foo\t2005-11-21
234
1 Joe Foo\t2005-11-21
240
class TestLongLogFormatter(TestCaseWithTransport):
242
def normalize_log(self,log):
243
"""Replaces the variable lines of logs with fixed lines"""
244
committer = 'committer: Lorem Ipsum <test@example.com>'
245
lines = log.splitlines(True)
246
for idx,line in enumerate(lines):
247
stripped_line = line.lstrip()
248
indent = ' ' * (len(line) - len(stripped_line))
249
if stripped_line.startswith('committer:'):
250
lines[idx] = indent + committer + '\n'
251
if stripped_line.startswith('timestamp:'):
252
lines[idx] = indent + 'timestamp: Just now\n'
253
return ''.join(lines)
255
def test_verbose_log(self):
256
"""Verbose log includes changed files
260
wt = self.make_branch_and_tree('.')
262
self.build_tree(['a'])
264
# XXX: why does a longer nick show up?
265
b.nick = 'test_verbose_log'
266
wt.commit(message='add a',
267
timestamp=1132711707,
269
committer='Lorem Ipsum <test@example.com>')
270
logfile = file('out.tmp', 'w+')
271
formatter = LongLogFormatter(to_file=logfile)
272
show_log(b, formatter, verbose=True)
275
log_contents = logfile.read()
276
self.assertEqualDiff(log_contents, '''\
277
------------------------------------------------------------
279
committer: Lorem Ipsum <test@example.com>
280
branch nick: test_verbose_log
281
timestamp: Wed 2005-11-23 12:08:27 +1000
288
def test_merges_are_indented_by_level(self):
289
wt = self.make_branch_and_tree('parent')
290
wt.commit('first post')
291
self.run_bzr('branch', 'parent', 'child')
292
self.run_bzr('commit', '-m', 'branch 1', '--unchanged', 'child')
293
self.run_bzr('branch', 'child', 'smallerchild')
294
self.run_bzr('commit', '-m', 'branch 2', '--unchanged', 'smallerchild')
296
self.run_bzr('merge', '../smallerchild')
297
self.run_bzr('commit', '-m', 'merge branch 2')
298
os.chdir('../parent')
299
self.run_bzr('merge', '../child')
300
wt.commit('merge branch 1')
303
lf = LongLogFormatter(to_file=sio)
304
show_log(b, lf, verbose=True)
305
log = self.normalize_log(sio.getvalue())
306
self.assertEqualDiff("""\
307
------------------------------------------------------------
309
committer: Lorem Ipsum <test@example.com>
314
------------------------------------------------------------
316
committer: Lorem Ipsum <test@example.com>
321
------------------------------------------------------------
323
committer: Lorem Ipsum <test@example.com>
324
branch nick: smallerchild
328
------------------------------------------------------------
330
committer: Lorem Ipsum <test@example.com>
335
------------------------------------------------------------
337
committer: Lorem Ipsum <test@example.com>
344
def test_verbose_merge_revisions_contain_deltas(self):
345
wt = self.make_branch_and_tree('parent')
346
self.build_tree(['parent/f1', 'parent/f2'])
348
wt.commit('first post')
349
self.run_bzr('branch', 'parent', 'child')
350
os.unlink('child/f1')
351
print >> file('child/f2', 'wb'), 'hello'
352
self.run_bzr('commit', '-m', 'removed f1 and modified f2', 'child')
354
self.run_bzr('merge', '../child')
355
wt.commit('merge branch 1')
358
lf = LongLogFormatter(to_file=sio)
359
show_log(b, lf, verbose=True)
360
log = self.normalize_log(sio.getvalue())
361
self.assertEqualDiff("""\
362
------------------------------------------------------------
364
committer: Lorem Ipsum <test@example.com>
373
------------------------------------------------------------
375
committer: Lorem Ipsum <test@example.com>
379
removed f1 and modified f2
384
------------------------------------------------------------
386
committer: Lorem Ipsum <test@example.com>
396
def test_trailing_newlines(self):
397
wt = self.make_branch_and_tree('.')
398
b = make_commits_with_trailing_newlines(wt)
400
lf = LongLogFormatter(to_file=sio)
402
self.assertEqualDiff(sio.getvalue(), """\
403
------------------------------------------------------------
405
committer: Joe Foo <joe@foo.com>
407
timestamp: Mon 2005-11-21 09:32:56 -0600
409
single line with trailing newline
410
------------------------------------------------------------
412
committer: Joe Foo <joe@foo.com>
414
timestamp: Mon 2005-11-21 09:27:22 -0600
419
------------------------------------------------------------
421
committer: Joe Foo <joe@foo.com>
423
timestamp: Mon 2005-11-21 09:24:15 -0600
429
class TestLineLogFormatter(TestCaseWithTransport):
431
def test_line_log(self):
432
"""Line log should show revno
436
wt = self.make_branch_and_tree('.')
438
self.build_tree(['a'])
440
b.nick = 'test-line-log'
441
wt.commit(message='add a',
442
timestamp=1132711707,
444
committer='Line-Log-Formatter Tester <test@line.log>')
445
logfile = file('out.tmp', 'w+')
446
formatter = LineLogFormatter(to_file=logfile)
447
show_log(b, formatter)
450
log_contents = logfile.read()
451
self.assertEqualDiff(log_contents, '1: Line-Log-Formatte... 2005-11-23 add a\n')
453
def test_short_log_with_merges(self):
454
wt = self.make_branch_and_memory_tree('.')
458
wt.commit('rev-1', rev_id='rev-1',
459
timestamp=1132586655, timezone=36000,
460
committer='Joe Foo <joe@foo.com>')
461
wt.commit('rev-merged', rev_id='rev-2a',
462
timestamp=1132586700, timezone=36000,
463
committer='Joe Foo <joe@foo.com>')
464
wt.set_parent_ids(['rev-1', 'rev-2a'])
465
wt.branch.set_last_revision_info(1, 'rev-1')
466
wt.commit('rev-2', rev_id='rev-2b',
467
timestamp=1132586800, timezone=36000,
468
committer='Joe Foo <joe@foo.com>')
470
formatter = ShortLogFormatter(to_file=logfile)
471
show_log(wt.branch, formatter)
473
self.assertEqualDiff("""\
474
2 Joe Foo\t2005-11-22 [merge]
477
1 Joe Foo\t2005-11-22
480
""", logfile.getvalue())
484
def test_trailing_newlines(self):
485
wt = self.make_branch_and_tree('.')
486
b = make_commits_with_trailing_newlines(wt)
488
lf = LineLogFormatter(to_file=sio)
490
self.assertEqualDiff(sio.getvalue(), """\
491
3: Joe Foo 2005-11-21 single line with trailing newline
492
2: Joe Foo 2005-11-21 multiline
493
1: Joe Foo 2005-11-21 simple log message
497
class TestGetViewRevisions(TestCaseWithTransport):
499
def make_tree_with_commits(self):
500
"""Create a tree with well-known revision ids"""
501
wt = self.make_branch_and_tree('tree1')
502
wt.commit('commit one', rev_id='1')
503
wt.commit('commit two', rev_id='2')
504
wt.commit('commit three', rev_id='3')
505
mainline_revs = [None, '1', '2', '3']
506
rev_nos = {'1': 1, '2': 2, '3': 3}
507
return mainline_revs, rev_nos, wt
509
def make_tree_with_merges(self):
510
"""Create a tree with well-known revision ids and a merge"""
511
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
512
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
513
tree2.commit('four-a', rev_id='4a')
514
wt.merge_from_branch(tree2.branch)
515
wt.commit('four-b', rev_id='4b')
516
mainline_revs.append('4b')
519
return mainline_revs, rev_nos, wt
521
def make_tree_with_many_merges(self):
522
"""Create a tree with well-known revision ids"""
523
wt = self.make_branch_and_tree('tree1')
524
wt.commit('commit one', rev_id='1')
525
wt.commit('commit two', rev_id='2')
526
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
527
tree3.commit('commit three a', rev_id='3a')
528
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
529
tree2.merge_from_branch(tree3.branch)
530
tree2.commit('commit three b', rev_id='3b')
531
wt.merge_from_branch(tree2.branch)
532
wt.commit('commit three c', rev_id='3c')
533
tree2.commit('four-a', rev_id='4a')
534
wt.merge_from_branch(tree2.branch)
535
wt.commit('four-b', rev_id='4b')
536
mainline_revs = [None, '1', '2', '3c', '4b']
537
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
538
full_rev_nos_for_reference = {
541
'3a': '2.2.1', #first commit tree 3
542
'3b': '2.1.1', # first commit tree 2
543
'3c': '3', #merges 3b to main
544
'4a': '2.1.2', # second commit tree 2
545
'4b': '4', # merges 4a to main
547
return mainline_revs, rev_nos, wt
549
def test_get_view_revisions_forward(self):
550
"""Test the get_view_revisions method"""
551
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
552
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
554
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
556
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
557
'forward', include_merges=False))
558
self.assertEqual(revisions, revisions2)
560
def test_get_view_revisions_reverse(self):
561
"""Test the get_view_revisions with reverse"""
562
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
563
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
565
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
567
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
568
'reverse', include_merges=False))
569
self.assertEqual(revisions, revisions2)
571
def test_get_view_revisions_merge(self):
572
"""Test get_view_revisions when there are merges"""
573
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
574
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
576
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
577
('4b', '4', 0), ('4a', '3.1.1', 1)],
579
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
580
'forward', include_merges=False))
581
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
585
def test_get_view_revisions_merge_reverse(self):
586
"""Test get_view_revisions in reverse when there are merges"""
587
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
588
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
590
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
591
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
593
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
594
'reverse', include_merges=False))
595
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
599
def test_get_view_revisions_merge2(self):
600
"""Test get_view_revisions when there are merges"""
601
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
602
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
604
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
605
('3a', '2.2.1', 1), ('3b', '2.1.1', 1), ('4b', '4', 0),
607
self.assertEqual(expected, revisions)
608
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
609
'forward', include_merges=False))
610
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
615
class TestGetRevisionsTouchingFileID(TestCaseWithTransport):
617
def create_tree_with_single_merge(self):
618
"""Create a branch with a moderate layout.
620
The revision graph looks like:
628
In this graph, A introduced files f1 and f2 and f3.
629
B modifies f1 and f3, and C modifies f2 and f3.
630
D merges the changes from B and C and resolves the conflict for f3.
632
# TODO: jam 20070218 This seems like it could really be done
633
# with make_branch_and_memory_tree() if we could just
634
# create the content of those files.
635
# TODO: jam 20070218 Another alternative is that we would really
636
# like to only create this tree 1 time for all tests that
637
# use it. Since 'log' only uses the tree in a readonly
638
# fashion, it seems a shame to regenerate an identical
639
# tree for each test.
640
tree = self.make_branch_and_tree('tree')
642
self.addCleanup(tree.unlock)
644
self.build_tree_contents([('tree/f1', 'A\n'),
648
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
649
tree.commit('A', rev_id='A')
651
self.build_tree_contents([('tree/f2', 'A\nC\n'),
652
('tree/f3', 'A\nC\n'),
654
tree.commit('C', rev_id='C')
655
# Revert back to A to build the other history.
656
tree.set_last_revision('A')
657
tree.branch.set_last_revision_info(1, 'A')
658
self.build_tree_contents([('tree/f1', 'A\nB\n'),
660
('tree/f3', 'A\nB\n'),
662
tree.commit('B', rev_id='B')
663
tree.set_parent_ids(['B', 'C'])
664
self.build_tree_contents([('tree/f1', 'A\nB\n'),
665
('tree/f2', 'A\nC\n'),
666
('tree/f3', 'A\nB\nC\n'),
668
tree.commit('D', rev_id='D')
670
# Switch to a read lock for this tree.
671
# We still have addCleanup(unlock)
676
def test_tree_with_single_merge(self):
677
"""Make sure the tree layout is correct."""
678
tree = self.create_tree_with_single_merge()
679
rev_A_tree = tree.branch.repository.revision_tree('A')
680
rev_B_tree = tree.branch.repository.revision_tree('B')
682
f1_changed = (u'f1', 'f1-id', 'file', True, False)
683
f2_changed = (u'f2', 'f2-id', 'file', True, False)
684
f3_changed = (u'f3', 'f3-id', 'file', True, False)
686
delta = rev_B_tree.changes_from(rev_A_tree)
687
self.assertEqual([f1_changed, f3_changed], delta.modified)
688
self.assertEqual([], delta.renamed)
689
self.assertEqual([], delta.added)
690
self.assertEqual([], delta.removed)
692
rev_C_tree = tree.branch.repository.revision_tree('C')
693
delta = rev_C_tree.changes_from(rev_A_tree)
694
self.assertEqual([f2_changed, f3_changed], delta.modified)
695
self.assertEqual([], delta.renamed)
696
self.assertEqual([], delta.added)
697
self.assertEqual([], delta.removed)
699
rev_D_tree = tree.branch.repository.revision_tree('D')
700
delta = rev_D_tree.changes_from(rev_B_tree)
701
self.assertEqual([f2_changed, f3_changed], delta.modified)
702
self.assertEqual([], delta.renamed)
703
self.assertEqual([], delta.added)
704
self.assertEqual([], delta.removed)
706
delta = rev_D_tree.changes_from(rev_C_tree)
707
self.assertEqual([f1_changed, f3_changed], delta.modified)
708
self.assertEqual([], delta.renamed)
709
self.assertEqual([], delta.added)
710
self.assertEqual([], delta.removed)
712
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
713
"""Make sure _filter_revisions_touching_file_id returns the right values.
715
Get the return value from _filter_revisions_touching_file_id and make
716
sure they are correct.
718
# The api for _get_revisions_touching_file_id is a little crazy,
719
# So we do the setup here.
720
mainline = tree.branch.revision_history()
721
mainline.insert(0, None)
722
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
723
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
725
actual_revs = log._filter_revisions_touching_file_id(
729
list(view_revs_iter))
730
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
732
def test_file_id_f1(self):
733
tree = self.create_tree_with_single_merge()
734
# f1 should be marked as modified by revisions A and B
735
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
737
def test_file_id_f2(self):
738
tree = self.create_tree_with_single_merge()
739
# f2 should be marked as modified by revisions A, C, and D
740
# because D merged the changes from C.
741
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
743
def test_file_id_f3(self):
744
tree = self.create_tree_with_single_merge()
745
# f3 should be marked as modified by revisions A, B, C, and D
746
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])