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 BzrTestBase, 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 SimpleLogTest(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, [])
60
# tests are written with unix paths; fix them up for windows
62
# expected = [x.replace('/', os.sep) for x in expected]
64
# strip out only the path components
65
got = [x[0] for x in getattr(delta, n)]
66
self.assertEquals(expected, got)
68
def test_cur_revno(self):
69
wt = self.make_branch_and_tree('.')
73
wt.commit('empty commit')
74
show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
75
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
76
start_revision=2, end_revision=1)
77
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
78
start_revision=1, end_revision=2)
79
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
80
start_revision=0, end_revision=2)
81
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
82
start_revision=1, end_revision=0)
83
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
84
start_revision=-1, end_revision=1)
85
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
86
start_revision=1, end_revision=-1)
88
def test_simple_log(self):
89
eq = self.assertEquals
91
wt = self.make_branch_and_tree('.')
99
wt.commit('empty commit')
101
show_log(b, lf, verbose=True)
103
eq(lf.logs[0].revno, '1')
104
eq(lf.logs[0].rev.message, 'empty commit')
106
self.log('log delta: %r' % d)
109
self.build_tree(['hello'])
111
wt.commit('add one file')
114
# log using regular thing
115
show_log(b, LongLogFormatter(lf))
117
for l in lf.readlines():
120
# get log as data structure
122
show_log(b, lf, verbose=True)
124
self.log('log entries:')
125
for logentry in lf.logs:
126
self.log('%4s %s' % (logentry.revno, logentry.rev.message))
128
# first one is most recent
129
logentry = lf.logs[0]
130
eq(logentry.revno, '2')
131
eq(logentry.rev.message, 'add one file')
133
self.log('log 2 delta: %r' % d)
134
# self.checkDelta(d, added=['hello'])
136
# commit a log message with control characters
137
msg = "All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
138
self.log("original commit message: %r", msg)
141
show_log(b, lf, verbose=True)
142
committed_msg = lf.logs[0].rev.message
143
self.log("escaped commit message: %r", committed_msg)
144
self.assert_(msg != committed_msg)
145
self.assert_(len(committed_msg) > len(msg))
147
# Check that log message with only XML-valid characters isn't
148
# escaped. As ElementTree apparently does some kind of
149
# newline conversion, neither LF (\x0A) nor CR (\x0D) are
150
# included in the test commit message, even though they are
151
# valid XML 1.0 characters.
152
msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
153
self.log("original commit message: %r", msg)
156
show_log(b, lf, verbose=True)
157
committed_msg = lf.logs[0].rev.message
158
self.log("escaped commit message: %r", committed_msg)
159
self.assert_(msg == committed_msg)
161
def test_trailing_newlines(self):
162
wt = self.make_branch_and_tree('.')
165
open('a', 'wb').write('hello moto\n')
167
wt.commit('simple log message', rev_id='a1'
168
, timestamp=1132586655.459960938, timezone=-6*3600
169
, committer='Joe Foo <joe@foo.com>')
170
open('b', 'wb').write('goodbye\n')
172
wt.commit('multiline\nlog\nmessage\n', rev_id='a2'
173
, timestamp=1132586842.411175966, timezone=-6*3600
174
, committer='Joe Foo <joe@foo.com>')
176
open('c', 'wb').write('just another manic monday\n')
178
wt.commit('single line with trailing newline\n', rev_id='a3'
179
, timestamp=1132587176.835228920, timezone=-6*3600
180
, committer = 'Joe Foo <joe@foo.com>')
183
lf = ShortLogFormatter(to_file=sio)
185
self.assertEquals(sio.getvalue(), """\
186
3 Joe Foo\t2005-11-21
187
single line with trailing newline
189
2 Joe Foo\t2005-11-21
194
1 Joe Foo\t2005-11-21
200
lf = LongLogFormatter(to_file=sio)
202
self.assertEquals(sio.getvalue(), """\
203
------------------------------------------------------------
205
committer: Joe Foo <joe@foo.com>
207
timestamp: Mon 2005-11-21 09:32:56 -0600
209
single line with trailing newline
210
------------------------------------------------------------
212
committer: Joe Foo <joe@foo.com>
214
timestamp: Mon 2005-11-21 09:27:22 -0600
219
------------------------------------------------------------
221
committer: Joe Foo <joe@foo.com>
223
timestamp: Mon 2005-11-21 09:24:15 -0600
228
def test_verbose_log(self):
229
"""Verbose log includes changed files
233
wt = self.make_branch_and_tree('.')
235
self.build_tree(['a'])
237
# XXX: why does a longer nick show up?
238
b.nick = 'test_verbose_log'
239
wt.commit(message='add a',
240
timestamp=1132711707,
242
committer='Lorem Ipsum <test@example.com>')
243
logfile = file('out.tmp', 'w+')
244
formatter = LongLogFormatter(to_file=logfile)
245
show_log(b, formatter, verbose=True)
248
log_contents = logfile.read()
249
self.assertEqualDiff(log_contents, '''\
250
------------------------------------------------------------
252
committer: Lorem Ipsum <test@example.com>
253
branch nick: test_verbose_log
254
timestamp: Wed 2005-11-23 12:08:27 +1000
261
def test_line_log(self):
262
"""Line log should show revno
266
wt = self.make_branch_and_tree('.')
268
self.build_tree(['a'])
270
b.nick = 'test-line-log'
271
wt.commit(message='add a',
272
timestamp=1132711707,
274
committer='Line-Log-Formatter Tester <test@line.log>')
275
logfile = file('out.tmp', 'w+')
276
formatter = LineLogFormatter(to_file=logfile)
277
show_log(b, formatter)
280
log_contents = logfile.read()
281
self.assertEqualDiff(log_contents, '1: Line-Log-Formatte... 2005-11-23 add a\n')
283
def test_short_log_with_merges(self):
284
wt = self.make_branch_and_memory_tree('.')
288
wt.commit('rev-1', rev_id='rev-1',
289
timestamp=1132586655, timezone=36000,
290
committer='Joe Foo <joe@foo.com>')
291
wt.commit('rev-merged', rev_id='rev-2a',
292
timestamp=1132586700, timezone=36000,
293
committer='Joe Foo <joe@foo.com>')
294
wt.set_parent_ids(['rev-1', 'rev-2a'])
295
wt.branch.set_last_revision_info(1, 'rev-1')
296
wt.commit('rev-2', rev_id='rev-2b',
297
timestamp=1132586800, timezone=36000,
298
committer='Joe Foo <joe@foo.com>')
300
formatter = ShortLogFormatter(to_file=logfile)
301
show_log(wt.branch, formatter)
303
self.assertEqualDiff("""\
304
2 Joe Foo\t2005-11-22 [merge]
307
1 Joe Foo\t2005-11-22
310
""", logfile.getvalue())
314
def make_tree_with_commits(self):
315
"""Create a tree with well-known revision ids"""
316
wt = self.make_branch_and_tree('tree1')
317
wt.commit('commit one', rev_id='1')
318
wt.commit('commit two', rev_id='2')
319
wt.commit('commit three', rev_id='3')
320
mainline_revs = [None, '1', '2', '3']
321
rev_nos = {'1': 1, '2': 2, '3': 3}
322
return mainline_revs, rev_nos, wt
324
def make_tree_with_merges(self):
325
"""Create a tree with well-known revision ids and a merge"""
326
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
327
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
328
tree2.commit('four-a', rev_id='4a')
329
wt.merge_from_branch(tree2.branch)
330
wt.commit('four-b', rev_id='4b')
331
mainline_revs.append('4b')
334
return mainline_revs, rev_nos, wt
336
def make_tree_with_many_merges(self):
337
"""Create a tree with well-known revision ids"""
338
wt = self.make_branch_and_tree('tree1')
339
wt.commit('commit one', rev_id='1')
340
wt.commit('commit two', rev_id='2')
341
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
342
tree3.commit('commit three a', rev_id='3a')
343
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
344
tree2.merge_from_branch(tree3.branch)
345
tree2.commit('commit three b', rev_id='3b')
346
wt.merge_from_branch(tree2.branch)
347
wt.commit('commit three c', rev_id='3c')
348
tree2.commit('four-a', rev_id='4a')
349
wt.merge_from_branch(tree2.branch)
350
wt.commit('four-b', rev_id='4b')
351
mainline_revs = [None, '1', '2', '3c', '4b']
352
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
353
full_rev_nos_for_reference = {
356
'3a': '2.2.1', #first commit tree 3
357
'3b': '2.1.1', # first commit tree 2
358
'3c': '3', #merges 3b to main
359
'4a': '2.1.2', # second commit tree 2
360
'4b': '4', # merges 4a to main
362
return mainline_revs, rev_nos, wt
364
def test_get_view_revisions_forward(self):
365
"""Test the get_view_revisions method"""
366
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
367
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
369
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
371
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
372
'forward', include_merges=False))
373
self.assertEqual(revisions, revisions2)
375
def test_get_view_revisions_reverse(self):
376
"""Test the get_view_revisions with reverse"""
377
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
378
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
380
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
382
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
383
'reverse', include_merges=False))
384
self.assertEqual(revisions, revisions2)
386
def test_get_view_revisions_merge(self):
387
"""Test get_view_revisions when there are merges"""
388
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
389
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
391
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
392
('4b', '4', 0), ('4a', '3.1.1', 1)],
394
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
395
'forward', include_merges=False))
396
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
400
def test_get_view_revisions_merge_reverse(self):
401
"""Test get_view_revisions in reverse when there are merges"""
402
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
403
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
405
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
406
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
408
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
409
'reverse', include_merges=False))
410
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
414
def test_get_view_revisions_merge2(self):
415
"""Test get_view_revisions when there are merges"""
416
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
417
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
419
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
420
('3a', '2.2.1', 1), ('3b', '2.1.1', 1), ('4b', '4', 0),
422
self.assertEqual(expected, revisions)
423
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
424
'forward', include_merges=False))
425
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
430
class TestGetRevisionsTouchingFileID(TestCaseWithTransport):
432
def create_tree_with_single_merge(self):
433
"""Create a branch with a moderate layout.
435
The revision graph looks like:
443
In this graph, A introduced files f1 and f2 and f3.
444
B modifies f1 and f3, and C modifies f2 and f3.
445
D merges the changes from B and C and resolves the conflict for f3.
447
# TODO: jam 20070218 This seems like it could really be done
448
# with make_branch_and_memory_tree() if we could just
449
# create the content of those files.
450
# TODO: jam 20070218 Another alternative is that we would really
451
# like to only create this tree 1 time for all tests that
452
# use it. Since 'log' only uses the tree in a readonly
453
# fashion, it seems a shame to regenerate an identical
454
# tree for each test.
455
tree = self.make_branch_and_tree('tree')
457
self.addCleanup(tree.unlock)
459
self.build_tree_contents([('tree/f1', 'A\n'),
463
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
464
tree.commit('A', rev_id='A')
466
self.build_tree_contents([('tree/f2', 'A\nC\n'),
467
('tree/f3', 'A\nC\n'),
469
tree.commit('C', rev_id='C')
470
# Revert back to A to build the other history.
471
tree.set_last_revision('A')
472
tree.branch.set_last_revision_info(1, 'A')
473
self.build_tree_contents([('tree/f1', 'A\nB\n'),
475
('tree/f3', 'A\nB\n'),
477
tree.commit('B', rev_id='B')
478
tree.set_parent_ids(['B', 'C'])
479
self.build_tree_contents([('tree/f1', 'A\nB\n'),
480
('tree/f2', 'A\nC\n'),
481
('tree/f3', 'A\nB\nC\n'),
483
tree.commit('D', rev_id='D')
485
# Switch to a read lock for this tree.
486
# We still have addCleanup(unlock)
491
def test_tree_with_single_merge(self):
492
"""Make sure the tree layout is correct."""
493
tree = self.create_tree_with_single_merge()
494
rev_A_tree = tree.branch.repository.revision_tree('A')
495
rev_B_tree = tree.branch.repository.revision_tree('B')
497
f1_changed = (u'f1', 'f1-id', 'file', True, False)
498
f2_changed = (u'f2', 'f2-id', 'file', True, False)
499
f3_changed = (u'f3', 'f3-id', 'file', True, False)
501
delta = rev_B_tree.changes_from(rev_A_tree)
502
self.assertEqual([f1_changed, f3_changed], delta.modified)
503
self.assertEqual([], delta.renamed)
504
self.assertEqual([], delta.added)
505
self.assertEqual([], delta.removed)
507
rev_C_tree = tree.branch.repository.revision_tree('C')
508
delta = rev_C_tree.changes_from(rev_A_tree)
509
self.assertEqual([f2_changed, f3_changed], delta.modified)
510
self.assertEqual([], delta.renamed)
511
self.assertEqual([], delta.added)
512
self.assertEqual([], delta.removed)
514
rev_D_tree = tree.branch.repository.revision_tree('D')
515
delta = rev_D_tree.changes_from(rev_B_tree)
516
self.assertEqual([f2_changed, f3_changed], delta.modified)
517
self.assertEqual([], delta.renamed)
518
self.assertEqual([], delta.added)
519
self.assertEqual([], delta.removed)
521
delta = rev_D_tree.changes_from(rev_C_tree)
522
self.assertEqual([f1_changed, f3_changed], delta.modified)
523
self.assertEqual([], delta.renamed)
524
self.assertEqual([], delta.added)
525
self.assertEqual([], delta.removed)
527
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
528
"""Make sure _get_revisions_touching_file_id returns the right values.
530
Get the return value from _get_revisions_touching_file_id and make
531
sure they are correct.
533
# The api for _get_revisions_touching_file_id is a little crazy,
534
# So we do the setup here.
535
mainline = tree.branch.revision_history()
536
mainline.insert(0, None)
537
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
538
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
540
actual_revs = log._get_revisions_touching_file_id(tree.branch, file_id,
543
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
545
def test_file_id_f1(self):
546
tree = self.create_tree_with_single_merge()
547
# f1 should be marked as modified by revisions A and B
548
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
550
def test_file_id_f2(self):
551
tree = self.create_tree_with_single_merge()
552
# f2 should be marked as modified by revisions A, C, and D
553
# because D merged the changes from C.
554
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
556
def test_file_id_f3(self):
557
tree = self.create_tree_with_single_merge()
558
# f3 should be marked as modified by revisions A, B, C, and D
559
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])