121
97
eq(len(lf.logs), 2)
122
98
self.log('log entries:')
123
99
for logentry in lf.logs:
124
self.log('%4s %s' % (logentry.revno, logentry.rev.message))
100
self.log('%4d %s' % (logentry.revno, logentry.rev.message))
126
102
# first one is most recent
127
103
logentry = lf.logs[0]
128
eq(logentry.revno, '2')
104
eq(logentry.revno, 2)
129
105
eq(logentry.rev.message, 'add one file')
130
106
d = logentry.delta
131
107
self.log('log 2 delta: %r' % d)
132
self.checkDelta(d, added=['hello'])
134
# commit a log message with control characters
135
msg = "All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
136
self.log("original commit message: %r", msg)
139
show_log(b, lf, verbose=True)
140
committed_msg = lf.logs[0].rev.message
141
self.log("escaped commit message: %r", committed_msg)
142
self.assert_(msg != committed_msg)
143
self.assert_(len(committed_msg) > len(msg))
145
# Check that log message with only XML-valid characters isn't
146
# escaped. As ElementTree apparently does some kind of
147
# newline conversion, neither LF (\x0A) nor CR (\x0D) are
148
# included in the test commit message, even though they are
149
# valid XML 1.0 characters.
150
msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
151
self.log("original commit message: %r", msg)
154
show_log(b, lf, verbose=True)
155
committed_msg = lf.logs[0].rev.message
156
self.log("escaped commit message: %r", committed_msg)
157
self.assert_(msg == committed_msg)
159
def test_deltas_in_merge_revisions(self):
160
"""Check deltas created for both mainline and merge revisions"""
161
eq = self.assertEquals
162
wt = self.make_branch_and_tree('parent')
163
self.build_tree(['parent/file1', 'parent/file2', 'parent/file3'])
166
wt.commit(message='add file1 and file2')
167
self.run_bzr('branch parent child')
168
os.unlink('child/file1')
169
file('child/file2', 'wb').write('hello\n')
170
self.run_bzr(['commit', '-m', 'remove file1 and modify file2',
173
self.run_bzr('merge ../child')
174
wt.commit('merge child branch')
178
lf.supports_merge_revisions = True
179
show_log(b, lf, verbose=True)
181
logentry = lf.logs[0]
182
eq(logentry.revno, '2')
183
eq(logentry.rev.message, 'merge child branch')
185
self.checkDelta(d, removed=['file1'], modified=['file2'])
186
logentry = lf.logs[1]
187
eq(logentry.revno, '1.1.1')
188
eq(logentry.rev.message, 'remove file1 and modify file2')
190
self.checkDelta(d, removed=['file1'], modified=['file2'])
191
logentry = lf.logs[2]
192
eq(logentry.revno, '1')
193
eq(logentry.rev.message, 'add file1 and file2')
195
self.checkDelta(d, added=['file1', 'file2'])
198
def make_commits_with_trailing_newlines(wt):
199
"""Helper method for LogFormatter tests"""
202
open('a', 'wb').write('hello moto\n')
204
wt.commit('simple log message', rev_id='a1',
205
timestamp=1132586655.459960938, timezone=-6*3600,
206
committer='Joe Foo <joe@foo.com>')
207
open('b', 'wb').write('goodbye\n')
209
wt.commit('multiline\nlog\nmessage\n', rev_id='a2',
210
timestamp=1132586842.411175966, timezone=-6*3600,
211
committer='Joe Foo <joe@foo.com>',
212
author='Joe Bar <joe@bar.com>')
214
open('c', 'wb').write('just another manic monday\n')
216
wt.commit('single line with trailing newline\n', rev_id='a3',
217
timestamp=1132587176.835228920, timezone=-6*3600,
218
committer = 'Joe Foo <joe@foo.com>')
222
def normalize_log(log):
223
"""Replaces the variable lines of logs with fixed lines"""
224
author = 'author: Dolor Sit <test@example.com>'
225
committer = 'committer: Lorem Ipsum <test@example.com>'
226
lines = log.splitlines(True)
227
for idx,line in enumerate(lines):
228
stripped_line = line.lstrip()
229
indent = ' ' * (len(line) - len(stripped_line))
230
if stripped_line.startswith('author:'):
231
lines[idx] = indent + author + '\n'
232
elif stripped_line.startswith('committer:'):
233
lines[idx] = indent + committer + '\n'
234
elif stripped_line.startswith('timestamp:'):
235
lines[idx] = indent + 'timestamp: Just now\n'
236
return ''.join(lines)
239
class TestShortLogFormatter(TestCaseWithTransport):
241
def test_trailing_newlines(self):
242
wt = self.make_branch_and_tree('.')
243
b = make_commits_with_trailing_newlines(wt)
244
sio = self.make_utf8_encoded_stringio()
245
lf = ShortLogFormatter(to_file=sio)
247
self.assertEqualDiff(sio.getvalue(), """\
248
3 Joe Foo\t2005-11-21
249
single line with trailing newline
251
2 Joe Bar\t2005-11-21
256
1 Joe Foo\t2005-11-21
262
class TestLongLogFormatter(TestCaseWithTransport):
264
def test_verbose_log(self):
265
"""Verbose log includes changed files
269
wt = self.make_branch_and_tree('.')
271
self.build_tree(['a'])
273
# XXX: why does a longer nick show up?
274
b.nick = 'test_verbose_log'
275
wt.commit(message='add a',
276
timestamp=1132711707,
278
committer='Lorem Ipsum <test@example.com>')
279
logfile = file('out.tmp', 'w+')
280
formatter = LongLogFormatter(to_file=logfile)
281
show_log(b, formatter, verbose=True)
284
log_contents = logfile.read()
285
self.assertEqualDiff(log_contents, '''\
286
------------------------------------------------------------
288
committer: Lorem Ipsum <test@example.com>
289
branch nick: test_verbose_log
290
timestamp: Wed 2005-11-23 12:08:27 +1000
297
def test_merges_are_indented_by_level(self):
298
wt = self.make_branch_and_tree('parent')
299
wt.commit('first post')
300
self.run_bzr('branch parent child')
301
self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
302
self.run_bzr('branch child smallerchild')
303
self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
306
self.run_bzr('merge ../smallerchild')
307
self.run_bzr(['commit', '-m', 'merge branch 2'])
308
os.chdir('../parent')
309
self.run_bzr('merge ../child')
310
wt.commit('merge branch 1')
312
sio = self.make_utf8_encoded_stringio()
313
lf = LongLogFormatter(to_file=sio)
314
show_log(b, lf, verbose=True)
315
log = normalize_log(sio.getvalue())
316
self.assertEqualDiff(log, """\
317
------------------------------------------------------------
319
committer: Lorem Ipsum <test@example.com>
324
------------------------------------------------------------
326
committer: Lorem Ipsum <test@example.com>
331
------------------------------------------------------------
333
committer: Lorem Ipsum <test@example.com>
334
branch nick: smallerchild
338
------------------------------------------------------------
340
committer: Lorem Ipsum <test@example.com>
345
------------------------------------------------------------
347
committer: Lorem Ipsum <test@example.com>
354
def test_verbose_merge_revisions_contain_deltas(self):
355
wt = self.make_branch_and_tree('parent')
356
self.build_tree(['parent/f1', 'parent/f2'])
358
wt.commit('first post')
359
self.run_bzr('branch parent child')
360
os.unlink('child/f1')
361
file('child/f2', 'wb').write('hello\n')
362
self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
365
self.run_bzr('merge ../child')
366
wt.commit('merge branch 1')
368
sio = self.make_utf8_encoded_stringio()
369
lf = LongLogFormatter(to_file=sio)
370
show_log(b, lf, verbose=True)
371
log = normalize_log(sio.getvalue())
372
self.assertEqualDiff(log, """\
373
------------------------------------------------------------
375
committer: Lorem Ipsum <test@example.com>
384
------------------------------------------------------------
386
committer: Lorem Ipsum <test@example.com>
390
removed f1 and modified f2
395
------------------------------------------------------------
397
committer: Lorem Ipsum <test@example.com>
407
def test_trailing_newlines(self):
408
wt = self.make_branch_and_tree('.')
409
b = make_commits_with_trailing_newlines(wt)
410
sio = self.make_utf8_encoded_stringio()
411
lf = LongLogFormatter(to_file=sio)
413
self.assertEqualDiff(sio.getvalue(), """\
414
------------------------------------------------------------
416
committer: Joe Foo <joe@foo.com>
418
timestamp: Mon 2005-11-21 09:32:56 -0600
420
single line with trailing newline
421
------------------------------------------------------------
423
author: Joe Bar <joe@bar.com>
424
committer: Joe Foo <joe@foo.com>
426
timestamp: Mon 2005-11-21 09:27:22 -0600
431
------------------------------------------------------------
433
committer: Joe Foo <joe@foo.com>
435
timestamp: Mon 2005-11-21 09:24:15 -0600
440
def test_author_in_log(self):
441
"""Log includes the author name if it's set in
442
the revision properties
444
wt = self.make_branch_and_tree('.')
446
self.build_tree(['a'])
448
b.nick = 'test_author_log'
449
wt.commit(message='add a',
450
timestamp=1132711707,
452
committer='Lorem Ipsum <test@example.com>',
453
author='John Doe <jdoe@example.com>')
455
formatter = LongLogFormatter(to_file=sio)
456
show_log(b, formatter)
457
self.assertEqualDiff(sio.getvalue(), '''\
458
------------------------------------------------------------
460
author: John Doe <jdoe@example.com>
461
committer: Lorem Ipsum <test@example.com>
462
branch nick: test_author_log
463
timestamp: Wed 2005-11-23 12:08:27 +1000
470
class TestLineLogFormatter(TestCaseWithTransport):
472
def test_line_log(self):
473
"""Line log should show revno
477
wt = self.make_branch_and_tree('.')
479
self.build_tree(['a'])
481
b.nick = 'test-line-log'
482
wt.commit(message='add a',
483
timestamp=1132711707,
485
committer='Line-Log-Formatter Tester <test@line.log>')
486
logfile = file('out.tmp', 'w+')
487
formatter = LineLogFormatter(to_file=logfile)
488
show_log(b, formatter)
491
log_contents = logfile.read()
492
self.assertEqualDiff(log_contents,
493
'1: Line-Log-Formatte... 2005-11-23 add a\n')
495
def test_short_log_with_merges(self):
496
wt = self.make_branch_and_memory_tree('.')
500
wt.commit('rev-1', rev_id='rev-1',
501
timestamp=1132586655, timezone=36000,
502
committer='Joe Foo <joe@foo.com>')
503
wt.commit('rev-merged', rev_id='rev-2a',
504
timestamp=1132586700, timezone=36000,
505
committer='Joe Foo <joe@foo.com>')
506
wt.set_parent_ids(['rev-1', 'rev-2a'])
507
wt.branch.set_last_revision_info(1, 'rev-1')
508
wt.commit('rev-2', rev_id='rev-2b',
509
timestamp=1132586800, timezone=36000,
510
committer='Joe Foo <joe@foo.com>')
511
logfile = self.make_utf8_encoded_stringio()
512
formatter = ShortLogFormatter(to_file=logfile)
513
show_log(wt.branch, formatter)
515
self.assertEqualDiff(logfile.getvalue(), """\
516
2 Joe Foo\t2005-11-22 [merge]
519
1 Joe Foo\t2005-11-22
526
def test_trailing_newlines(self):
527
wt = self.make_branch_and_tree('.')
528
b = make_commits_with_trailing_newlines(wt)
529
sio = self.make_utf8_encoded_stringio()
530
lf = LineLogFormatter(to_file=sio)
532
self.assertEqualDiff(sio.getvalue(), """\
533
3: Joe Foo 2005-11-21 single line with trailing newline
534
2: Joe Bar 2005-11-21 multiline
535
1: Joe Foo 2005-11-21 simple log message
539
class TestGetViewRevisions(TestCaseWithTransport):
541
def make_tree_with_commits(self):
542
"""Create a tree with well-known revision ids"""
543
wt = self.make_branch_and_tree('tree1')
544
wt.commit('commit one', rev_id='1')
545
wt.commit('commit two', rev_id='2')
546
wt.commit('commit three', rev_id='3')
547
mainline_revs = [None, '1', '2', '3']
548
rev_nos = {'1': 1, '2': 2, '3': 3}
549
return mainline_revs, rev_nos, wt
551
def make_tree_with_merges(self):
552
"""Create a tree with well-known revision ids and a merge"""
553
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
554
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
555
tree2.commit('four-a', rev_id='4a')
556
wt.merge_from_branch(tree2.branch)
557
wt.commit('four-b', rev_id='4b')
558
mainline_revs.append('4b')
561
return mainline_revs, rev_nos, wt
563
def make_tree_with_many_merges(self):
564
"""Create a tree with well-known revision ids"""
565
wt = self.make_branch_and_tree('tree1')
566
wt.commit('commit one', rev_id='1')
567
wt.commit('commit two', rev_id='2')
568
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
569
tree3.commit('commit three a', rev_id='3a')
570
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
571
tree2.merge_from_branch(tree3.branch)
572
tree2.commit('commit three b', rev_id='3b')
573
wt.merge_from_branch(tree2.branch)
574
wt.commit('commit three c', rev_id='3c')
575
tree2.commit('four-a', rev_id='4a')
576
wt.merge_from_branch(tree2.branch)
577
wt.commit('four-b', rev_id='4b')
578
mainline_revs = [None, '1', '2', '3c', '4b']
579
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
580
full_rev_nos_for_reference = {
583
'3a': '2.2.1', #first commit tree 3
584
'3b': '2.1.1', # first commit tree 2
585
'3c': '3', #merges 3b to main
586
'4a': '2.1.2', # second commit tree 2
587
'4b': '4', # merges 4a to main
589
return mainline_revs, rev_nos, wt
591
def test_get_view_revisions_forward(self):
592
"""Test the get_view_revisions method"""
593
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
594
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
596
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
598
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
599
'forward', include_merges=False))
600
self.assertEqual(revisions, revisions2)
602
def test_get_view_revisions_reverse(self):
603
"""Test the get_view_revisions with reverse"""
604
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
605
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
607
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
609
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
610
'reverse', include_merges=False))
611
self.assertEqual(revisions, revisions2)
613
def test_get_view_revisions_merge(self):
614
"""Test get_view_revisions when there are merges"""
615
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
616
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
618
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
619
('4b', '4', 0), ('4a', '3.1.1', 1)],
621
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
622
'forward', include_merges=False))
623
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
627
def test_get_view_revisions_merge_reverse(self):
628
"""Test get_view_revisions in reverse when there are merges"""
629
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
630
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
632
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
633
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
635
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
636
'reverse', include_merges=False))
637
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
641
def test_get_view_revisions_merge2(self):
642
"""Test get_view_revisions when there are merges"""
643
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
644
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
646
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
647
('3a', '2.2.1', 1), ('3b', '2.1.1', 1), ('4b', '4', 0),
649
self.assertEqual(expected, revisions)
650
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
651
'forward', include_merges=False))
652
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
657
class TestGetRevisionsTouchingFileID(TestCaseWithTransport):
659
def create_tree_with_single_merge(self):
660
"""Create a branch with a moderate layout.
662
The revision graph looks like:
670
In this graph, A introduced files f1 and f2 and f3.
671
B modifies f1 and f3, and C modifies f2 and f3.
672
D merges the changes from B and C and resolves the conflict for f3.
674
# TODO: jam 20070218 This seems like it could really be done
675
# with make_branch_and_memory_tree() if we could just
676
# create the content of those files.
677
# TODO: jam 20070218 Another alternative is that we would really
678
# like to only create this tree 1 time for all tests that
679
# use it. Since 'log' only uses the tree in a readonly
680
# fashion, it seems a shame to regenerate an identical
681
# tree for each test.
682
tree = self.make_branch_and_tree('tree')
684
self.addCleanup(tree.unlock)
686
self.build_tree_contents([('tree/f1', 'A\n'),
690
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
691
tree.commit('A', rev_id='A')
693
self.build_tree_contents([('tree/f2', 'A\nC\n'),
694
('tree/f3', 'A\nC\n'),
696
tree.commit('C', rev_id='C')
697
# Revert back to A to build the other history.
698
tree.set_last_revision('A')
699
tree.branch.set_last_revision_info(1, 'A')
700
self.build_tree_contents([('tree/f1', 'A\nB\n'),
702
('tree/f3', 'A\nB\n'),
704
tree.commit('B', rev_id='B')
705
tree.set_parent_ids(['B', 'C'])
706
self.build_tree_contents([('tree/f1', 'A\nB\n'),
707
('tree/f2', 'A\nC\n'),
708
('tree/f3', 'A\nB\nC\n'),
710
tree.commit('D', rev_id='D')
712
# Switch to a read lock for this tree.
713
# We still have addCleanup(unlock)
718
def test_tree_with_single_merge(self):
719
"""Make sure the tree layout is correct."""
720
tree = self.create_tree_with_single_merge()
721
rev_A_tree = tree.branch.repository.revision_tree('A')
722
rev_B_tree = tree.branch.repository.revision_tree('B')
724
f1_changed = (u'f1', 'f1-id', 'file', True, False)
725
f2_changed = (u'f2', 'f2-id', 'file', True, False)
726
f3_changed = (u'f3', 'f3-id', 'file', True, False)
728
delta = rev_B_tree.changes_from(rev_A_tree)
729
self.assertEqual([f1_changed, f3_changed], delta.modified)
730
self.assertEqual([], delta.renamed)
731
self.assertEqual([], delta.added)
732
self.assertEqual([], delta.removed)
734
rev_C_tree = tree.branch.repository.revision_tree('C')
735
delta = rev_C_tree.changes_from(rev_A_tree)
736
self.assertEqual([f2_changed, f3_changed], delta.modified)
737
self.assertEqual([], delta.renamed)
738
self.assertEqual([], delta.added)
739
self.assertEqual([], delta.removed)
741
rev_D_tree = tree.branch.repository.revision_tree('D')
742
delta = rev_D_tree.changes_from(rev_B_tree)
743
self.assertEqual([f2_changed, f3_changed], delta.modified)
744
self.assertEqual([], delta.renamed)
745
self.assertEqual([], delta.added)
746
self.assertEqual([], delta.removed)
748
delta = rev_D_tree.changes_from(rev_C_tree)
749
self.assertEqual([f1_changed, f3_changed], delta.modified)
750
self.assertEqual([], delta.renamed)
751
self.assertEqual([], delta.added)
752
self.assertEqual([], delta.removed)
754
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
755
"""Make sure _filter_revisions_touching_file_id returns the right values.
757
Get the return value from _filter_revisions_touching_file_id and make
758
sure they are correct.
760
# The api for _get_revisions_touching_file_id is a little crazy,
761
# So we do the setup here.
762
mainline = tree.branch.revision_history()
763
mainline.insert(0, None)
764
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
765
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
767
actual_revs = log._filter_revisions_touching_file_id(
771
list(view_revs_iter))
772
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
774
def test_file_id_f1(self):
775
tree = self.create_tree_with_single_merge()
776
# f1 should be marked as modified by revisions A and B
777
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
779
def test_file_id_f2(self):
780
tree = self.create_tree_with_single_merge()
781
# f2 should be marked as modified by revisions A, C, and D
782
# because D merged the changes from C.
783
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
785
def test_file_id_f3(self):
786
tree = self.create_tree_with_single_merge()
787
# f3 should be marked as modified by revisions A, B, C, and D
788
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
791
class TestShowChangedRevisions(TestCaseWithTransport):
793
def test_show_changed_revisions_verbose(self):
794
tree = self.make_branch_and_tree('tree_a')
795
self.build_tree(['tree_a/foo'])
797
tree.commit('bar', rev_id='bar-id')
798
s = self.make_utf8_encoded_stringio()
799
log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
800
self.assertContainsRe(s.getvalue(), 'bar')
801
self.assertNotContainsRe(s.getvalue(), 'foo')
804
class TestLogFormatter(TestCase):
806
def test_short_committer(self):
807
rev = Revision('a-id')
808
rev.committer = 'John Doe <jdoe@example.com>'
809
lf = LogFormatter(None)
810
self.assertEqual('John Doe', lf.short_committer(rev))
812
def test_short_author(self):
813
rev = Revision('a-id')
814
rev.committer = 'John Doe <jdoe@example.com>'
815
lf = LogFormatter(None)
816
self.assertEqual('John Doe', lf.short_author(rev))
817
rev.properties['author'] = 'John Smith <jsmith@example.com>'
818
self.assertEqual('John Smith', lf.short_author(rev))
108
# self.checkDelta(d, added=['hello'])