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 make_tree_with_commits(self):
284
"""Create a tree with well-known revision ids"""
285
wt = self.make_branch_and_tree('tree1')
286
wt.commit('commit one', rev_id='1')
287
wt.commit('commit two', rev_id='2')
288
wt.commit('commit three', rev_id='3')
289
mainline_revs = [None, '1', '2', '3']
290
rev_nos = {'1': 1, '2': 2, '3': 3}
291
return mainline_revs, rev_nos, wt
293
def make_tree_with_merges(self):
294
"""Create a tree with well-known revision ids and a merge"""
295
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
296
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
297
tree2.commit('four-a', rev_id='4a')
298
wt.merge_from_branch(tree2.branch)
299
wt.commit('four-b', rev_id='4b')
300
mainline_revs.append('4b')
303
return mainline_revs, rev_nos, wt
305
def make_tree_with_many_merges(self):
306
"""Create a tree with well-known revision ids"""
307
wt = self.make_branch_and_tree('tree1')
308
wt.commit('commit one', rev_id='1')
309
wt.commit('commit two', rev_id='2')
310
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
311
tree3.commit('commit three a', rev_id='3a')
312
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
313
tree2.merge_from_branch(tree3.branch)
314
tree2.commit('commit three b', rev_id='3b')
315
wt.merge_from_branch(tree2.branch)
316
wt.commit('commit three c', rev_id='3c')
317
tree2.commit('four-a', rev_id='4a')
318
wt.merge_from_branch(tree2.branch)
319
wt.commit('four-b', rev_id='4b')
320
mainline_revs = [None, '1', '2', '3c', '4b']
321
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
322
full_rev_nos_for_reference = {
325
'3a': '2.2.1', #first commit tree 3
326
'3b': '2.1.1', # first commit tree 2
327
'3c': '3', #merges 3b to main
328
'4a': '2.1.2', # second commit tree 2
329
'4b': '4', # merges 4a to main
331
return mainline_revs, rev_nos, wt
333
def test_get_view_revisions_forward(self):
334
"""Test the get_view_revisions method"""
335
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
336
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
338
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
340
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
341
'forward', include_merges=False))
342
self.assertEqual(revisions, revisions2)
344
def test_get_view_revisions_reverse(self):
345
"""Test the get_view_revisions with reverse"""
346
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
347
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
349
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
351
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
352
'reverse', include_merges=False))
353
self.assertEqual(revisions, revisions2)
355
def test_get_view_revisions_merge(self):
356
"""Test get_view_revisions when there are merges"""
357
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
358
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
360
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
361
('4b', '4', 0), ('4a', '3.1.1', 1)],
363
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
364
'forward', include_merges=False))
365
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
369
def test_get_view_revisions_merge_reverse(self):
370
"""Test get_view_revisions in reverse when there are merges"""
371
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
372
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
374
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
375
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
377
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
378
'reverse', include_merges=False))
379
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
383
def test_get_view_revisions_merge2(self):
384
"""Test get_view_revisions when there are merges"""
385
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
386
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
388
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
389
('3a', '2.2.1', 1), ('3b', '2.1.1', 1), ('4b', '4', 0),
391
self.assertEqual(expected, revisions)
392
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
393
'forward', include_merges=False))
394
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
399
class TestGetRevisionsTouchingFileID(TestCaseWithTransport):
401
def create_tree_with_single_merge(self):
402
"""Create a branch with a moderate layout.
404
The revision graph looks like:
412
In this graph, A introduced files f1 and f2 and f3.
413
B modifies f1 and f3, and C modifies f2 and f3.
414
D merges the changes from B and C and resolves the conflict for f3.
416
# TODO: jam 20070218 This seems like it could really be done
417
# with make_branch_and_memory_tree() if we could just
418
# create the content of those files.
419
# TODO: jam 20070218 Another alternative is that we would really
420
# like to only create this tree 1 time for all tests that
421
# use it. Since 'log' only uses the tree in a readonly
422
# fashion, it seems a shame to regenerate an identical
423
# tree for each test.
424
tree = self.make_branch_and_tree('tree')
426
self.addCleanup(tree.unlock)
428
self.build_tree_contents([('tree/f1', 'A\n'),
432
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
433
tree.commit('A', rev_id='A')
435
self.build_tree_contents([('tree/f2', 'A\nC\n'),
436
('tree/f3', 'A\nC\n'),
438
tree.commit('C', rev_id='C')
439
# Revert back to A to build the other history.
440
tree.set_last_revision('A')
441
tree.branch.set_last_revision_info(1, 'A')
442
self.build_tree_contents([('tree/f1', 'A\nB\n'),
444
('tree/f3', 'A\nB\n'),
446
tree.commit('B', rev_id='B')
447
tree.set_parent_ids(['B', 'C'])
448
self.build_tree_contents([('tree/f1', 'A\nB\n'),
449
('tree/f2', 'A\nC\n'),
450
('tree/f3', 'A\nB\nC\n'),
452
tree.commit('D', rev_id='D')
454
# Switch to a read lock for this tree.
455
# We still have addCleanup(unlock)
460
def test_tree_with_single_merge(self):
461
"""Make sure the tree layout is correct."""
462
tree = self.create_tree_with_single_merge()
463
rev_A_tree = tree.branch.repository.revision_tree('A')
464
rev_B_tree = tree.branch.repository.revision_tree('B')
466
f1_changed = (u'f1', 'f1-id', 'file', True, False)
467
f2_changed = (u'f2', 'f2-id', 'file', True, False)
468
f3_changed = (u'f3', 'f3-id', 'file', True, False)
470
delta = rev_B_tree.changes_from(rev_A_tree)
471
self.assertEqual([f1_changed, f3_changed], delta.modified)
472
self.assertEqual([], delta.renamed)
473
self.assertEqual([], delta.added)
474
self.assertEqual([], delta.removed)
476
rev_C_tree = tree.branch.repository.revision_tree('C')
477
delta = rev_C_tree.changes_from(rev_A_tree)
478
self.assertEqual([f2_changed, f3_changed], delta.modified)
479
self.assertEqual([], delta.renamed)
480
self.assertEqual([], delta.added)
481
self.assertEqual([], delta.removed)
483
rev_D_tree = tree.branch.repository.revision_tree('D')
484
delta = rev_D_tree.changes_from(rev_B_tree)
485
self.assertEqual([f2_changed, f3_changed], delta.modified)
486
self.assertEqual([], delta.renamed)
487
self.assertEqual([], delta.added)
488
self.assertEqual([], delta.removed)
490
delta = rev_D_tree.changes_from(rev_C_tree)
491
self.assertEqual([f1_changed, f3_changed], delta.modified)
492
self.assertEqual([], delta.renamed)
493
self.assertEqual([], delta.added)
494
self.assertEqual([], delta.removed)
496
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
497
"""Make sure _get_revisions_touching_file_id returns the right values.
499
Get the return value from _get_revisions_touching_file_id and make
500
sure they are correct.
502
# The api for _get_revisions_touching_file_id is a little crazy,
503
# So we do the setup here.
504
mainline = tree.branch.revision_history()
505
mainline.insert(0, None)
506
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
507
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
509
actual_revs = log._get_revisions_touching_file_id(tree.branch, file_id,
512
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
514
def test_file_id_f1(self):
515
tree = self.create_tree_with_single_merge()
516
# f1 should be marked as modified by revisions A and B
517
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
519
def test_file_id_f2(self):
520
tree = self.create_tree_with_single_merge()
521
# f2 should be marked as modified by revisions A, C, and D
522
# because D merged the changes from C.
523
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
525
def test_file_id_f3(self):
526
tree = self.create_tree_with_single_merge()
527
# f3 should be marked as modified by revisions A, B, C, and D
528
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])