52
37
We should also test the LogFormatter.
57
39
def __init__(self):
58
40
super(LogCatcher, self).__init__(to_file=None)
61
def log_revision(self, revision):
62
self.logs.append(revision)
65
class TestShowLog(tests.TestCaseWithTransport):
44
def show(self, revno, rev, delta):
52
class SimpleLogTest(TestCaseWithTransport):
67
54
def checkDelta(self, delta, **kw):
68
"""Check the filenames touched by a delta are as expected.
70
Caller only have to pass in the list of files for each part, all
71
unspecified parts are considered empty (and checked as such).
55
"""Check the filenames touched by a delta are as expected."""
73
56
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
74
# By default we expect an empty list
75
57
expected = kw.get(n, [])
59
# tests are written with unix paths; fix them up for windows
61
# expected = [x.replace('/', os.sep) for x in expected]
76
63
# strip out only the path components
77
64
got = [x[0] for x in getattr(delta, n)]
78
self.assertEqual(expected, got)
80
def assertInvalidRevisonNumber(self, br, start, end):
82
self.assertRaises(errors.InvalidRevisionNumber,
84
start_revision=start, end_revision=end)
86
def test_cur_revno(self):
87
wt = self.make_branch_and_tree('.')
91
wt.commit('empty commit')
92
log.show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
94
# Since there is a single revision in the branch all the combinations
96
self.assertInvalidRevisonNumber(b, 2, 1)
97
self.assertInvalidRevisonNumber(b, 1, 2)
98
self.assertInvalidRevisonNumber(b, 0, 2)
99
self.assertInvalidRevisonNumber(b, 1, 0)
100
self.assertInvalidRevisonNumber(b, -1, 1)
101
self.assertInvalidRevisonNumber(b, 1, -1)
103
def test_empty_branch(self):
104
wt = self.make_branch_and_tree('.')
107
log.show_log(wt.branch, lf)
65
self.assertEquals(expected, got)
67
def test_cur_revno(self):
68
wt = self.make_branch_and_tree('.')
72
wt.commit('empty commit')
73
show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
74
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
75
start_revision=2, end_revision=1)
76
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
77
start_revision=1, end_revision=2)
78
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
79
start_revision=0, end_revision=2)
80
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
81
start_revision=1, end_revision=0)
82
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
83
start_revision=-1, end_revision=1)
84
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
85
start_revision=1, end_revision=-1)
87
def test_cur_revno(self):
88
wt = self.make_branch_and_tree('.')
92
wt.commit('empty commit')
93
show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
94
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
95
start_revision=2, end_revision=1)
96
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
97
start_revision=1, end_revision=2)
98
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
99
start_revision=0, end_revision=2)
100
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
101
start_revision=1, end_revision=0)
102
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
103
start_revision=-1, end_revision=1)
104
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
105
start_revision=1, end_revision=-1)
107
def test_simple_log(self):
108
eq = self.assertEquals
110
wt = self.make_branch_and_tree('.')
109
self.assertEqual([], lf.logs)
111
def test_empty_commit(self):
112
wt = self.make_branch_and_tree('.')
114
118
wt.commit('empty commit')
115
119
lf = LogCatcher()
116
log.show_log(wt.branch, lf, verbose=True)
117
self.assertEqual(1, len(lf.logs))
118
self.assertEqual('1', lf.logs[0].revno)
119
self.assertEqual('empty commit', lf.logs[0].rev.message)
120
self.checkDelta(lf.logs[0].delta)
120
show_log(b, lf, verbose=True)
122
eq(lf.logs[0].revno, 1)
123
eq(lf.logs[0].rev.message, 'empty commit')
125
self.log('log delta: %r' % d)
122
def test_simple_commit(self):
123
wt = self.make_branch_and_tree('.')
124
wt.commit('empty commit')
125
128
self.build_tree(['hello'])
127
wt.commit('add one file',
128
committer=u'\u013d\xf3r\xe9m \xcdp\u0161\xfam '
129
u'<test@example.com>')
130
wt.commit('add one file')
133
# log using regular thing
134
show_log(b, LongLogFormatter(lf))
136
for l in lf.readlines():
139
# get log as data structure
130
140
lf = LogCatcher()
131
log.show_log(wt.branch, lf, verbose=True)
132
self.assertEqual(2, len(lf.logs))
141
show_log(b, lf, verbose=True)
143
self.log('log entries:')
144
for logentry in lf.logs:
145
self.log('%4d %s' % (logentry.revno, logentry.rev.message))
133
147
# first one is most recent
134
log_entry = lf.logs[0]
135
self.assertEqual('2', log_entry.revno)
136
self.assertEqual('add one file', log_entry.rev.message)
137
self.checkDelta(log_entry.delta, added=['hello'])
139
def test_commit_message_with_control_chars(self):
140
wt = self.make_branch_and_tree('.')
141
msg = u"All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
142
msg = msg.replace(u'\r', u'\n')
148
logentry = lf.logs[0]
149
eq(logentry.revno, 2)
150
eq(logentry.rev.message, 'add one file')
152
self.log('log 2 delta: %r' % d)
153
# self.checkDelta(d, added=['hello'])
155
# commit a log message with control characters
156
msg = "All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
157
self.log("original commit message: %r", msg)
144
159
lf = LogCatcher()
145
log.show_log(wt.branch, lf, verbose=True)
160
show_log(b, lf, verbose=True)
146
161
committed_msg = lf.logs[0].rev.message
147
self.assertNotEqual(msg, committed_msg)
148
self.assertTrue(len(committed_msg) > len(msg))
162
self.log("escaped commit message: %r", committed_msg)
163
self.assert_(msg != committed_msg)
164
self.assert_(len(committed_msg) > len(msg))
150
def test_commit_message_without_control_chars(self):
151
wt = self.make_branch_and_tree('.')
166
# Check that log message with only XML-valid characters isn't
152
167
# escaped. As ElementTree apparently does some kind of
153
168
# newline conversion, neither LF (\x0A) nor CR (\x0D) are
154
169
# included in the test commit message, even though they are
155
170
# valid XML 1.0 characters.
156
171
msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
172
self.log("original commit message: %r", msg)
158
174
lf = LogCatcher()
159
log.show_log(wt.branch, lf, verbose=True)
175
show_log(b, lf, verbose=True)
160
176
committed_msg = lf.logs[0].rev.message
161
self.assertEqual(msg, committed_msg)
163
def test_deltas_in_merge_revisions(self):
164
"""Check deltas created for both mainline and merge revisions"""
165
wt = self.make_branch_and_tree('parent')
166
self.build_tree(['parent/file1', 'parent/file2', 'parent/file3'])
169
wt.commit(message='add file1 and file2')
170
self.run_bzr('branch parent child')
171
os.unlink('child/file1')
172
file('child/file2', 'wb').write('hello\n')
173
self.run_bzr(['commit', '-m', 'remove file1 and modify file2',
176
self.run_bzr('merge ../child')
177
wt.commit('merge child branch')
181
lf.supports_merge_revisions = True
182
log.show_log(b, lf, verbose=True)
184
self.assertEqual(3, len(lf.logs))
186
logentry = lf.logs[0]
187
self.assertEqual('2', logentry.revno)
188
self.assertEqual('merge child branch', logentry.rev.message)
189
self.checkDelta(logentry.delta, removed=['file1'], modified=['file2'])
191
logentry = lf.logs[1]
192
self.assertEqual('1.1.1', logentry.revno)
193
self.assertEqual('remove file1 and modify file2', logentry.rev.message)
194
self.checkDelta(logentry.delta, removed=['file1'], modified=['file2'])
196
logentry = lf.logs[2]
197
self.assertEqual('1', logentry.revno)
198
self.assertEqual('add file1 and file2', logentry.rev.message)
199
self.checkDelta(logentry.delta, added=['file1', 'file2'])
201
def test_merges_nonsupporting_formatter(self):
202
"""Tests that show_log will raise if the formatter doesn't
203
support merge revisions."""
204
wt = self.make_branch_and_memory_tree('.')
206
self.addCleanup(wt.unlock)
208
wt.commit('rev-1', rev_id='rev-1',
209
timestamp=1132586655, timezone=36000,
210
committer='Joe Foo <joe@foo.com>')
211
wt.commit('rev-merged', rev_id='rev-2a',
212
timestamp=1132586700, timezone=36000,
213
committer='Joe Foo <joe@foo.com>')
214
wt.set_parent_ids(['rev-1', 'rev-2a'])
215
wt.branch.set_last_revision_info(1, 'rev-1')
216
wt.commit('rev-2', rev_id='rev-2b',
217
timestamp=1132586800, timezone=36000,
218
committer='Joe Foo <joe@foo.com>')
219
logfile = self.make_utf8_encoded_stringio()
220
formatter = log.ShortLogFormatter(to_file=logfile)
223
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
224
rev = revspec.in_history(wtb)
225
self.assertRaises(errors.BzrCommandError, log.show_log, wtb, lf,
226
start_revision=rev, end_revision=rev)
229
def make_commits_with_trailing_newlines(wt):
230
"""Helper method for LogFormatter tests"""
233
open('a', 'wb').write('hello moto\n')
235
wt.commit('simple log message', rev_id='a1',
236
timestamp=1132586655.459960938, timezone=-6*3600,
237
committer='Joe Foo <joe@foo.com>')
238
open('b', 'wb').write('goodbye\n')
240
wt.commit('multiline\nlog\nmessage\n', rev_id='a2',
241
timestamp=1132586842.411175966, timezone=-6*3600,
242
committer='Joe Foo <joe@foo.com>',
243
author='Joe Bar <joe@bar.com>')
245
open('c', 'wb').write('just another manic monday\n')
247
wt.commit('single line with trailing newline\n', rev_id='a3',
248
timestamp=1132587176.835228920, timezone=-6*3600,
249
committer = 'Joe Foo <joe@foo.com>')
253
def normalize_log(log):
254
"""Replaces the variable lines of logs with fixed lines"""
255
author = 'author: Dolor Sit <test@example.com>'
256
committer = 'committer: Lorem Ipsum <test@example.com>'
257
lines = log.splitlines(True)
258
for idx,line in enumerate(lines):
259
stripped_line = line.lstrip()
260
indent = ' ' * (len(line) - len(stripped_line))
261
if stripped_line.startswith('author:'):
262
lines[idx] = indent + author + '\n'
263
elif stripped_line.startswith('committer:'):
264
lines[idx] = indent + committer + '\n'
265
elif stripped_line.startswith('timestamp:'):
266
lines[idx] = indent + 'timestamp: Just now\n'
267
return ''.join(lines)
270
class TestShortLogFormatter(tests.TestCaseWithTransport):
177
self.log("escaped commit message: %r", committed_msg)
178
self.assert_(msg == committed_msg)
272
180
def test_trailing_newlines(self):
273
181
wt = self.make_branch_and_tree('.')
274
b = make_commits_with_trailing_newlines(wt)
275
sio = self.make_utf8_encoded_stringio()
276
lf = log.ShortLogFormatter(to_file=sio)
278
self.assertEqualDiff("""\
184
open('a', 'wb').write('hello moto\n')
186
wt.commit('simple log message', rev_id='a1'
187
, timestamp=1132586655.459960938, timezone=-6*3600
188
, committer='Joe Foo <joe@foo.com>')
189
open('b', 'wb').write('goodbye\n')
191
wt.commit('multiline\nlog\nmessage\n', rev_id='a2'
192
, timestamp=1132586842.411175966, timezone=-6*3600
193
, committer='Joe Foo <joe@foo.com>')
195
open('c', 'wb').write('just another manic monday\n')
197
wt.commit('single line with trailing newline\n', rev_id='a3'
198
, timestamp=1132587176.835228920, timezone=-6*3600
199
, committer = 'Joe Foo <joe@foo.com>')
202
lf = ShortLogFormatter(to_file=sio)
204
self.assertEquals(sio.getvalue(), """\
279
205
3 Joe Foo\t2005-11-21
280
206
single line with trailing newline
282
2 Joe Bar\t2005-11-21
208
2 Joe Foo\t2005-11-21
287
213
1 Joe Foo\t2005-11-21
288
214
simple log message
293
def test_short_log_with_merges(self):
294
wt = self.make_branch_and_memory_tree('.')
296
self.addCleanup(wt.unlock)
298
wt.commit('rev-1', rev_id='rev-1',
299
timestamp=1132586655, timezone=36000,
300
committer='Joe Foo <joe@foo.com>')
301
wt.commit('rev-merged', rev_id='rev-2a',
302
timestamp=1132586700, timezone=36000,
303
committer='Joe Foo <joe@foo.com>')
304
wt.set_parent_ids(['rev-1', 'rev-2a'])
305
wt.branch.set_last_revision_info(1, 'rev-1')
306
wt.commit('rev-2', rev_id='rev-2b',
307
timestamp=1132586800, timezone=36000,
308
committer='Joe Foo <joe@foo.com>')
309
logfile = self.make_utf8_encoded_stringio()
310
formatter = log.ShortLogFormatter(to_file=logfile)
311
log.show_log(wt.branch, formatter)
312
self.assertEqualDiff("""\
313
2 Joe Foo\t2005-11-22 [merge]
316
1 Joe Foo\t2005-11-22
322
def test_short_log_single_merge_revision(self):
323
wt = self.make_branch_and_memory_tree('.')
325
self.addCleanup(wt.unlock)
327
wt.commit('rev-1', rev_id='rev-1',
328
timestamp=1132586655, timezone=36000,
329
committer='Joe Foo <joe@foo.com>')
330
wt.commit('rev-merged', rev_id='rev-2a',
331
timestamp=1132586700, timezone=36000,
332
committer='Joe Foo <joe@foo.com>')
333
wt.set_parent_ids(['rev-1', 'rev-2a'])
334
wt.branch.set_last_revision_info(1, 'rev-1')
335
wt.commit('rev-2', rev_id='rev-2b',
336
timestamp=1132586800, timezone=36000,
337
committer='Joe Foo <joe@foo.com>')
338
logfile = self.make_utf8_encoded_stringio()
339
formatter = log.ShortLogFormatter(to_file=logfile)
340
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
342
rev = revspec.in_history(wtb)
343
log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
344
self.assertEqualDiff("""\
345
1.1.1 Joe Foo\t2005-11-22
352
class TestLongLogFormatter(TestCaseWithoutPropsHandler):
354
def test_verbose_log(self):
355
"""Verbose log includes changed files
359
wt = self.make_branch_and_tree('.')
361
self.build_tree(['a'])
363
# XXX: why does a longer nick show up?
364
b.nick = 'test_verbose_log'
365
wt.commit(message='add a',
366
timestamp=1132711707,
368
committer='Lorem Ipsum <test@example.com>')
369
logfile = file('out.tmp', 'w+')
370
formatter = log.LongLogFormatter(to_file=logfile)
371
log.show_log(b, formatter, verbose=True)
374
log_contents = logfile.read()
375
self.assertEqualDiff('''\
376
------------------------------------------------------------
378
committer: Lorem Ipsum <test@example.com>
379
branch nick: test_verbose_log
380
timestamp: Wed 2005-11-23 12:08:27 +1000
388
def test_merges_are_indented_by_level(self):
389
wt = self.make_branch_and_tree('parent')
390
wt.commit('first post')
391
self.run_bzr('branch parent child')
392
self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
393
self.run_bzr('branch child smallerchild')
394
self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
397
self.run_bzr('merge ../smallerchild')
398
self.run_bzr(['commit', '-m', 'merge branch 2'])
399
os.chdir('../parent')
400
self.run_bzr('merge ../child')
401
wt.commit('merge branch 1')
403
sio = self.make_utf8_encoded_stringio()
404
lf = log.LongLogFormatter(to_file=sio)
405
log.show_log(b, lf, verbose=True)
406
the_log = normalize_log(sio.getvalue())
407
self.assertEqualDiff("""\
408
------------------------------------------------------------
410
committer: Lorem Ipsum <test@example.com>
415
------------------------------------------------------------
417
committer: Lorem Ipsum <test@example.com>
422
------------------------------------------------------------
424
committer: Lorem Ipsum <test@example.com>
425
branch nick: smallerchild
429
------------------------------------------------------------
431
committer: Lorem Ipsum <test@example.com>
436
------------------------------------------------------------
438
committer: Lorem Ipsum <test@example.com>
446
def test_verbose_merge_revisions_contain_deltas(self):
447
wt = self.make_branch_and_tree('parent')
448
self.build_tree(['parent/f1', 'parent/f2'])
450
wt.commit('first post')
451
self.run_bzr('branch parent child')
452
os.unlink('child/f1')
453
file('child/f2', 'wb').write('hello\n')
454
self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
457
self.run_bzr('merge ../child')
458
wt.commit('merge branch 1')
460
sio = self.make_utf8_encoded_stringio()
461
lf = log.LongLogFormatter(to_file=sio)
462
log.show_log(b, lf, verbose=True)
463
the_log = normalize_log(sio.getvalue())
464
self.assertEqualDiff("""\
465
------------------------------------------------------------
467
committer: Lorem Ipsum <test@example.com>
476
------------------------------------------------------------
478
committer: Lorem Ipsum <test@example.com>
482
removed f1 and modified f2
487
------------------------------------------------------------
489
committer: Lorem Ipsum <test@example.com>
500
def test_trailing_newlines(self):
501
wt = self.make_branch_and_tree('.')
502
b = make_commits_with_trailing_newlines(wt)
503
sio = self.make_utf8_encoded_stringio()
504
lf = log.LongLogFormatter(to_file=sio)
506
self.assertEqualDiff("""\
219
lf = LongLogFormatter(to_file=sio)
221
self.assertEquals(sio.getvalue(), """\
507
222
------------------------------------------------------------
509
224
committer: Joe Foo <joe@foo.com>
528
242
timestamp: Mon 2005-11-21 09:24:15 -0600
530
244
simple log message
534
def test_author_in_log(self):
535
"""Log includes the author name if it's set in
536
the revision properties
538
wt = self.make_branch_and_tree('.')
540
self.build_tree(['a'])
542
b.nick = 'test_author_log'
543
wt.commit(message='add a',
544
timestamp=1132711707,
546
committer='Lorem Ipsum <test@example.com>',
547
author='John Doe <jdoe@example.com>')
549
formatter = log.LongLogFormatter(to_file=sio)
550
log.show_log(b, formatter)
551
self.assertEqualDiff('''\
552
------------------------------------------------------------
554
author: John Doe <jdoe@example.com>
555
committer: Lorem Ipsum <test@example.com>
556
branch nick: test_author_log
557
timestamp: Wed 2005-11-23 12:08:27 +1000
563
def test_properties_in_log(self):
564
"""Log includes the custom properties returned by the registered
567
wt = self.make_branch_and_tree('.')
569
self.build_tree(['a'])
571
b.nick = 'test_properties_in_log'
572
wt.commit(message='add a',
573
timestamp=1132711707,
575
committer='Lorem Ipsum <test@example.com>',
576
author='John Doe <jdoe@example.com>')
578
formatter = log.LongLogFormatter(to_file=sio)
580
def trivial_custom_prop_handler(revision):
581
return {'test_prop':'test_value'}
583
log.properties_handler_registry.register(
584
'trivial_custom_prop_handler',
585
trivial_custom_prop_handler)
586
log.show_log(b, formatter)
588
log.properties_handler_registry.remove(
589
'trivial_custom_prop_handler')
590
self.assertEqualDiff('''\
591
------------------------------------------------------------
593
test_prop: test_value
594
author: John Doe <jdoe@example.com>
595
committer: Lorem Ipsum <test@example.com>
596
branch nick: test_properties_in_log
597
timestamp: Wed 2005-11-23 12:08:27 +1000
603
def test_error_in_properties_handler(self):
604
"""Log includes the custom properties returned by the registered
607
wt = self.make_branch_and_tree('.')
609
self.build_tree(['a'])
611
b.nick = 'test_author_log'
612
wt.commit(message='add a',
613
timestamp=1132711707,
615
committer='Lorem Ipsum <test@example.com>',
616
author='John Doe <jdoe@example.com>',
617
revprops={'first_prop':'first_value'})
619
formatter = log.LongLogFormatter(to_file=sio)
621
def trivial_custom_prop_handler(revision):
622
raise StandardError("a test error")
624
log.properties_handler_registry.register(
625
'trivial_custom_prop_handler',
626
trivial_custom_prop_handler)
627
self.assertRaises(StandardError, log.show_log, b, formatter,)
629
log.properties_handler_registry.remove(
630
'trivial_custom_prop_handler')
632
def test_properties_handler_bad_argument(self):
633
wt = self.make_branch_and_tree('.')
635
self.build_tree(['a'])
637
b.nick = 'test_author_log'
638
wt.commit(message='add a',
639
timestamp=1132711707,
641
committer='Lorem Ipsum <test@example.com>',
642
author='John Doe <jdoe@example.com>',
643
revprops={'a_prop':'test_value'})
645
formatter = log.LongLogFormatter(to_file=sio)
647
def bad_argument_prop_handler(revision):
648
return {'custom_prop_name':revision.properties['a_prop']}
650
log.properties_handler_registry.register(
651
'bad_argument_prop_handler',
652
bad_argument_prop_handler)
654
self.assertRaises(AttributeError, formatter.show_properties,
657
revision = b.repository.get_revision(b.last_revision())
658
formatter.show_properties(revision, '')
659
self.assertEqualDiff('''custom_prop_name: test_value\n''',
662
log.properties_handler_registry.remove(
663
'bad_argument_prop_handler')
666
class TestLineLogFormatter(tests.TestCaseWithTransport):
668
def test_line_log(self):
669
"""Line log should show revno
673
wt = self.make_branch_and_tree('.')
675
self.build_tree(['a'])
677
b.nick = 'test-line-log'
678
wt.commit(message='add a',
679
timestamp=1132711707,
681
committer='Line-Log-Formatter Tester <test@line.log>')
247
def test_verbose_log(self):
248
"""Verbose log includes changed files
252
wt = self.make_branch_and_tree('.')
254
self.build_tree(['a'])
256
# XXX: why does a longer nick show up?
257
b.nick = 'test_verbose_log'
258
wt.commit(message='add a',
259
timestamp=1132711707,
261
committer='Lorem Ipsum <test@example.com>')
682
262
logfile = file('out.tmp', 'w+')
683
formatter = log.LineLogFormatter(to_file=logfile)
684
log.show_log(b, formatter)
263
formatter = LongLogFormatter(to_file=logfile)
264
show_log(b, formatter, verbose=True)
687
267
log_contents = logfile.read()
688
self.assertEqualDiff('1: Line-Log-Formatte... 2005-11-23 add a\n',
691
def test_trailing_newlines(self):
692
wt = self.make_branch_and_tree('.')
693
b = make_commits_with_trailing_newlines(wt)
694
sio = self.make_utf8_encoded_stringio()
695
lf = log.LineLogFormatter(to_file=sio)
697
self.assertEqualDiff("""\
698
3: Joe Foo 2005-11-21 single line with trailing newline
699
2: Joe Bar 2005-11-21 multiline
700
1: Joe Foo 2005-11-21 simple log message
704
def test_line_log_single_merge_revision(self):
705
wt = self.make_branch_and_memory_tree('.')
707
self.addCleanup(wt.unlock)
709
wt.commit('rev-1', rev_id='rev-1',
710
timestamp=1132586655, timezone=36000,
711
committer='Joe Foo <joe@foo.com>')
712
wt.commit('rev-merged', rev_id='rev-2a',
713
timestamp=1132586700, timezone=36000,
714
committer='Joe Foo <joe@foo.com>')
715
wt.set_parent_ids(['rev-1', 'rev-2a'])
716
wt.branch.set_last_revision_info(1, 'rev-1')
717
wt.commit('rev-2', rev_id='rev-2b',
718
timestamp=1132586800, timezone=36000,
719
committer='Joe Foo <joe@foo.com>')
720
logfile = self.make_utf8_encoded_stringio()
721
formatter = log.LineLogFormatter(to_file=logfile)
722
revspec = revisionspec.RevisionSpec.from_string('1.1.1')
724
rev = revspec.in_history(wtb)
725
log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
726
self.assertEqualDiff("""\
727
1.1.1: Joe Foo 2005-11-22 rev-merged
733
class TestGetViewRevisions(tests.TestCaseWithTransport):
735
def make_tree_with_commits(self):
736
"""Create a tree with well-known revision ids"""
737
wt = self.make_branch_and_tree('tree1')
738
wt.commit('commit one', rev_id='1')
739
wt.commit('commit two', rev_id='2')
740
wt.commit('commit three', rev_id='3')
741
mainline_revs = [None, '1', '2', '3']
742
rev_nos = {'1': 1, '2': 2, '3': 3}
743
return mainline_revs, rev_nos, wt
745
def make_tree_with_merges(self):
746
"""Create a tree with well-known revision ids and a merge"""
747
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
748
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
749
tree2.commit('four-a', rev_id='4a')
750
wt.merge_from_branch(tree2.branch)
751
wt.commit('four-b', rev_id='4b')
752
mainline_revs.append('4b')
755
return mainline_revs, rev_nos, wt
757
def make_tree_with_many_merges(self):
758
"""Create a tree with well-known revision ids"""
759
wt = self.make_branch_and_tree('tree1')
760
self.build_tree_contents([('tree1/f', '1\n')])
761
wt.add(['f'], ['f-id'])
762
wt.commit('commit one', rev_id='1')
763
wt.commit('commit two', rev_id='2')
765
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
766
self.build_tree_contents([('tree3/f', '1\n2\n3a\n')])
767
tree3.commit('commit three a', rev_id='3a')
769
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
770
tree2.merge_from_branch(tree3.branch)
771
tree2.commit('commit three b', rev_id='3b')
773
wt.merge_from_branch(tree2.branch)
774
wt.commit('commit three c', rev_id='3c')
775
tree2.commit('four-a', rev_id='4a')
777
wt.merge_from_branch(tree2.branch)
778
wt.commit('four-b', rev_id='4b')
780
mainline_revs = [None, '1', '2', '3c', '4b']
781
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
782
full_rev_nos_for_reference = {
785
'3a': '2.1.1', #first commit tree 3
786
'3b': '2.2.1', # first commit tree 2
787
'3c': '3', #merges 3b to main
788
'4a': '2.2.2', # second commit tree 2
789
'4b': '4', # merges 4a to main
791
return mainline_revs, rev_nos, wt
793
def test_get_view_revisions_forward(self):
794
"""Test the get_view_revisions method"""
795
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
797
self.addCleanup(wt.unlock)
798
revisions = list(log.get_view_revisions(
799
mainline_revs, rev_nos, wt.branch, 'forward'))
800
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
802
revisions2 = list(log.get_view_revisions(
803
mainline_revs, rev_nos, wt.branch, 'forward',
804
include_merges=False))
805
self.assertEqual(revisions, revisions2)
807
def test_get_view_revisions_reverse(self):
808
"""Test the get_view_revisions with reverse"""
809
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
811
self.addCleanup(wt.unlock)
812
revisions = list(log.get_view_revisions(
813
mainline_revs, rev_nos, wt.branch, 'reverse'))
814
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
816
revisions2 = list(log.get_view_revisions(
817
mainline_revs, rev_nos, wt.branch, 'reverse',
818
include_merges=False))
819
self.assertEqual(revisions, revisions2)
821
def test_get_view_revisions_merge(self):
822
"""Test get_view_revisions when there are merges"""
823
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
825
self.addCleanup(wt.unlock)
826
revisions = list(log.get_view_revisions(
827
mainline_revs, rev_nos, wt.branch, 'forward'))
828
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
829
('4b', '4', 0), ('4a', '3.1.1', 1)],
831
revisions = list(log.get_view_revisions(
832
mainline_revs, rev_nos, wt.branch, 'forward',
833
include_merges=False))
834
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
838
def test_get_view_revisions_merge_reverse(self):
839
"""Test get_view_revisions in reverse when there are merges"""
840
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
842
self.addCleanup(wt.unlock)
843
revisions = list(log.get_view_revisions(
844
mainline_revs, rev_nos, wt.branch, 'reverse'))
845
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
846
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
848
revisions = list(log.get_view_revisions(
849
mainline_revs, rev_nos, wt.branch, 'reverse',
850
include_merges=False))
851
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
855
def test_get_view_revisions_merge2(self):
856
"""Test get_view_revisions when there are merges"""
857
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
859
self.addCleanup(wt.unlock)
860
revisions = list(log.get_view_revisions(
861
mainline_revs, rev_nos, wt.branch, 'forward'))
862
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
863
('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
865
self.assertEqual(expected, revisions)
866
revisions = list(log.get_view_revisions(
867
mainline_revs, rev_nos, wt.branch, 'forward',
868
include_merges=False))
869
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
874
def test_file_id_for_range(self):
875
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
877
self.addCleanup(wt.unlock)
879
def rev_from_rev_id(revid, branch):
880
revspec = revisionspec.RevisionSpec.from_string('revid:%s' % revid)
881
return revspec.in_history(branch)
883
def view_revs(start_rev, end_rev, file_id, direction):
884
revs = log.calculate_view_revisions(
886
start_rev, # start_revision
887
end_rev, # end_revision
888
direction, # direction
889
file_id, # specific_fileid
890
True, # generate_merge_revisions
891
True, # allow_single_merge_revision
895
rev_3a = rev_from_rev_id('3a', wt.branch)
896
rev_4b = rev_from_rev_id('4b', wt.branch)
897
self.assertEqual([('3c', '3', 0), ('3a', '2.1.1', 1)],
898
view_revs(rev_3a, rev_4b, 'f-id', 'reverse'))
899
# Note that the depth is 0 for 3a because depths are normalized, but
900
# there is still a bug somewhere... most probably in
901
# _filter_revision_range and/or get_view_revisions still around a bad
902
# use of reverse_by_depth
903
self.assertEqual([('3a', '2.1.1', 0)],
904
view_revs(rev_3a, rev_4b, 'f-id', 'forward'))
907
class TestGetRevisionsTouchingFileID(tests.TestCaseWithTransport):
909
def create_tree_with_single_merge(self):
910
"""Create a branch with a moderate layout.
912
The revision graph looks like:
920
In this graph, A introduced files f1 and f2 and f3.
921
B modifies f1 and f3, and C modifies f2 and f3.
922
D merges the changes from B and C and resolves the conflict for f3.
924
# TODO: jam 20070218 This seems like it could really be done
925
# with make_branch_and_memory_tree() if we could just
926
# create the content of those files.
927
# TODO: jam 20070218 Another alternative is that we would really
928
# like to only create this tree 1 time for all tests that
929
# use it. Since 'log' only uses the tree in a readonly
930
# fashion, it seems a shame to regenerate an identical
931
# tree for each test.
932
tree = self.make_branch_and_tree('tree')
934
self.addCleanup(tree.unlock)
936
self.build_tree_contents([('tree/f1', 'A\n'),
940
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
941
tree.commit('A', rev_id='A')
943
self.build_tree_contents([('tree/f2', 'A\nC\n'),
944
('tree/f3', 'A\nC\n'),
946
tree.commit('C', rev_id='C')
947
# Revert back to A to build the other history.
948
tree.set_last_revision('A')
949
tree.branch.set_last_revision_info(1, 'A')
950
self.build_tree_contents([('tree/f1', 'A\nB\n'),
952
('tree/f3', 'A\nB\n'),
954
tree.commit('B', rev_id='B')
955
tree.set_parent_ids(['B', 'C'])
956
self.build_tree_contents([('tree/f1', 'A\nB\n'),
957
('tree/f2', 'A\nC\n'),
958
('tree/f3', 'A\nB\nC\n'),
960
tree.commit('D', rev_id='D')
962
# Switch to a read lock for this tree.
963
# We still have an addCleanup(tree.unlock) pending
968
def check_delta(self, delta, **kw):
969
"""Check the filenames touched by a delta are as expected.
971
Caller only have to pass in the list of files for each part, all
972
unspecified parts are considered empty (and checked as such).
974
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
975
# By default we expect an empty list
976
expected = kw.get(n, [])
977
# strip out only the path components
978
got = [x[0] for x in getattr(delta, n)]
979
self.assertEqual(expected, got)
981
def test_tree_with_single_merge(self):
982
"""Make sure the tree layout is correct."""
983
tree = self.create_tree_with_single_merge()
984
rev_A_tree = tree.branch.repository.revision_tree('A')
985
rev_B_tree = tree.branch.repository.revision_tree('B')
986
rev_C_tree = tree.branch.repository.revision_tree('C')
987
rev_D_tree = tree.branch.repository.revision_tree('D')
989
self.check_delta(rev_B_tree.changes_from(rev_A_tree),
990
modified=['f1', 'f3'])
992
self.check_delta(rev_C_tree.changes_from(rev_A_tree),
993
modified=['f2', 'f3'])
995
self.check_delta(rev_D_tree.changes_from(rev_B_tree),
996
modified=['f2', 'f3'])
998
self.check_delta(rev_D_tree.changes_from(rev_C_tree),
999
modified=['f1', 'f3'])
1001
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
1002
"""Ensure _filter_revisions_touching_file_id returns the right values.
1004
Get the return value from _filter_revisions_touching_file_id and make
1005
sure they are correct.
1007
# The api for _filter_revisions_touching_file_id is a little crazy.
1008
# So we do the setup here.
1009
mainline = tree.branch.revision_history()
1010
mainline.insert(0, None)
1011
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
1012
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
1014
actual_revs = log._filter_revisions_touching_file_id(
1017
list(view_revs_iter))
1018
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
1020
def test_file_id_f1(self):
1021
tree = self.create_tree_with_single_merge()
1022
# f1 should be marked as modified by revisions A and B
1023
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
1025
def test_file_id_f2(self):
1026
tree = self.create_tree_with_single_merge()
1027
# f2 should be marked as modified by revisions A, C, and D
1028
# because D merged the changes from C.
1029
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1031
def test_file_id_f3(self):
1032
tree = self.create_tree_with_single_merge()
1033
# f3 should be marked as modified by revisions A, B, C, and D
1034
self.assertAllRevisionsForFileID(tree, 'f3-id', ['D', 'C', 'B', 'A'])
1036
def test_file_id_with_ghosts(self):
1037
# This is testing bug #209948, where having a ghost would cause
1038
# _filter_revisions_touching_file_id() to fail.
1039
tree = self.create_tree_with_single_merge()
1040
# We need to add a revision, so switch back to a write-locked tree
1041
# (still a single addCleanup(tree.unlock) pending).
1044
first_parent = tree.last_revision()
1045
tree.set_parent_ids([first_parent, 'ghost-revision-id'])
1046
self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
1047
tree.commit('commit with a ghost', rev_id='XX')
1048
self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
1049
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1052
class TestShowChangedRevisions(tests.TestCaseWithTransport):
1054
def test_show_changed_revisions_verbose(self):
1055
tree = self.make_branch_and_tree('tree_a')
1056
self.build_tree(['tree_a/foo'])
1058
tree.commit('bar', rev_id='bar-id')
1059
s = self.make_utf8_encoded_stringio()
1060
log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
1061
self.assertContainsRe(s.getvalue(), 'bar')
1062
self.assertNotContainsRe(s.getvalue(), 'foo')
1065
class TestLogFormatter(tests.TestCase):
1067
def test_short_committer(self):
1068
rev = revision.Revision('a-id')
1069
rev.committer = 'John Doe <jdoe@example.com>'
1070
lf = log.LogFormatter(None)
1071
self.assertEqual('John Doe', lf.short_committer(rev))
1072
rev.committer = 'John Smith <jsmith@example.com>'
1073
self.assertEqual('John Smith', lf.short_committer(rev))
1074
rev.committer = 'John Smith'
1075
self.assertEqual('John Smith', lf.short_committer(rev))
1076
rev.committer = 'jsmith@example.com'
1077
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1078
rev.committer = '<jsmith@example.com>'
1079
self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1080
rev.committer = 'John Smith jsmith@example.com'
1081
self.assertEqual('John Smith', lf.short_committer(rev))
1083
def test_short_author(self):
1084
rev = revision.Revision('a-id')
1085
rev.committer = 'John Doe <jdoe@example.com>'
1086
lf = log.LogFormatter(None)
1087
self.assertEqual('John Doe', lf.short_author(rev))
1088
rev.properties['author'] = 'John Smith <jsmith@example.com>'
1089
self.assertEqual('John Smith', lf.short_author(rev))
1090
rev.properties['author'] = 'John Smith'
1091
self.assertEqual('John Smith', lf.short_author(rev))
1092
rev.properties['author'] = 'jsmith@example.com'
1093
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1094
rev.properties['author'] = '<jsmith@example.com>'
1095
self.assertEqual('jsmith@example.com', lf.short_author(rev))
1096
rev.properties['author'] = 'John Smith jsmith@example.com'
1097
self.assertEqual('John Smith', lf.short_author(rev))
1100
class TestReverseByDepth(tests.TestCase):
1101
"""Test reverse_by_depth behavior.
1103
This is used to present revisions in forward (oldest first) order in a nice
1106
The tests use lighter revision description to ease reading.
1109
def assertReversed(self, forward, backward):
1110
# Transform the descriptions to suit the API: tests use (revno, depth),
1111
# while the API expects (revid, revno, depth)
1112
def complete_revisions(l):
1113
"""Transform the description to suit the API.
1115
Tests use (revno, depth) whil the API expects (revid, revno, depth).
1116
Since the revid is arbitrary, we just duplicate revno
1118
return [ (r, r, d) for r, d in l]
1119
forward = complete_revisions(forward)
1120
backward= complete_revisions(backward)
1121
self.assertEqual(forward, log.reverse_by_depth(backward))
1124
def test_mainline_revisions(self):
1125
self.assertReversed([( '1', 0), ('2', 0)],
1126
[('2', 0), ('1', 0)])
1128
def test_merged_revisions(self):
1129
self.assertReversed([('1', 0), ('2', 0), ('2.2', 1), ('2.1', 1),],
1130
[('2', 0), ('2.1', 1), ('2.2', 1), ('1', 0),])
1131
def test_shifted_merged_revisions(self):
1132
"""Test irregular layout.
1134
Requesting revisions touching a file can produce "holes" in the depths.
1136
self.assertReversed([('1', 0), ('2', 0), ('1.1', 2), ('1.2', 2),],
1137
[('2', 0), ('1.2', 2), ('1.1', 2), ('1', 0),])
1139
def test_merged_without_child_revisions(self):
1140
"""Test irregular layout.
1142
Revision ranges can produce "holes" in the depths.
1144
# When a revision of higher depth doesn't follow one of lower depth, we
1145
# assume a lower depth one is virtually there
1146
self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1147
[('4', 4), ('3', 3), ('2', 2), ('1', 2),])
1148
# So we get the same order after reversing below even if the original
1149
# revisions are not in the same order.
1150
self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1151
[('3', 3), ('4', 4), ('2', 2), ('1', 2),])
268
self.assertEqualDiff(log_contents, '''\
269
------------------------------------------------------------
271
committer: Lorem Ipsum <test@example.com>
272
branch nick: test_verbose_log
273
timestamp: Wed 2005-11-23 12:08:27 +1000