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,
28
from bzrlib.branch import Branch
29
from bzrlib.errors import InvalidRevisionNumber
32
class _LogEntry(object):
33
# should probably move into bzrlib.log?
37
class LogCatcher(LogFormatter):
38
"""Pull log messages into list rather than displaying them.
40
For ease of testing we save log messages here rather than actually
41
formatting them, so that we can precisely check the result without
42
being too dependent on the exact formatting.
44
We should also test the LogFormatter.
47
super(LogCatcher, self).__init__(to_file=None)
50
def show(self, revno, rev, delta):
58
class SimpleLogTest(TestCaseWithTransport):
60
def checkDelta(self, delta, **kw):
61
"""Check the filenames touched by a delta are as expected."""
62
for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
63
expected = kw.get(n, [])
65
# tests are written with unix paths; fix them up for windows
67
# expected = [x.replace('/', os.sep) for x in expected]
69
# strip out only the path components
70
got = [x[0] for x in getattr(delta, n)]
71
self.assertEquals(expected, got)
73
def test_cur_revno(self):
74
wt = self.make_branch_and_tree('.')
78
wt.commit('empty commit')
79
show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
80
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
81
start_revision=2, end_revision=1)
82
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
83
start_revision=1, end_revision=2)
84
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
85
start_revision=0, end_revision=2)
86
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
87
start_revision=1, end_revision=0)
88
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
89
start_revision=-1, end_revision=1)
90
self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
91
start_revision=1, end_revision=-1)
93
def test_simple_log(self):
94
eq = self.assertEquals
96
wt = self.make_branch_and_tree('.')
104
wt.commit('empty commit')
106
show_log(b, lf, verbose=True)
108
eq(lf.logs[0].revno, '1')
109
eq(lf.logs[0].rev.message, 'empty commit')
111
self.log('log delta: %r' % d)
114
self.build_tree(['hello'])
116
wt.commit('add one file')
119
# log using regular thing
120
show_log(b, LongLogFormatter(lf))
122
for l in lf.readlines():
125
# get log as data structure
127
show_log(b, lf, verbose=True)
129
self.log('log entries:')
130
for logentry in lf.logs:
131
self.log('%4s %s' % (logentry.revno, logentry.rev.message))
133
# first one is most recent
134
logentry = lf.logs[0]
135
eq(logentry.revno, '2')
136
eq(logentry.rev.message, 'add one file')
138
self.log('log 2 delta: %r' % d)
139
# self.checkDelta(d, added=['hello'])
141
# commit a log message with control characters
142
msg = "All 8-bit chars: " + ''.join([unichr(x) for x in range(256)])
143
self.log("original commit message: %r", msg)
146
show_log(b, lf, verbose=True)
147
committed_msg = lf.logs[0].rev.message
148
self.log("escaped commit message: %r", committed_msg)
149
self.assert_(msg != committed_msg)
150
self.assert_(len(committed_msg) > len(msg))
152
# Check that log message with only XML-valid characters isn't
153
# escaped. As ElementTree apparently does some kind of
154
# newline conversion, neither LF (\x0A) nor CR (\x0D) are
155
# included in the test commit message, even though they are
156
# valid XML 1.0 characters.
157
msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
158
self.log("original commit message: %r", msg)
161
show_log(b, lf, verbose=True)
162
committed_msg = lf.logs[0].rev.message
163
self.log("escaped commit message: %r", committed_msg)
164
self.assert_(msg == committed_msg)
166
def test_trailing_newlines(self):
167
wt = self.make_branch_and_tree('.')
170
open('a', 'wb').write('hello moto\n')
172
wt.commit('simple log message', rev_id='a1'
173
, timestamp=1132586655.459960938, timezone=-6*3600
174
, committer='Joe Foo <joe@foo.com>')
175
open('b', 'wb').write('goodbye\n')
177
wt.commit('multiline\nlog\nmessage\n', rev_id='a2'
178
, timestamp=1132586842.411175966, timezone=-6*3600
179
, committer='Joe Foo <joe@foo.com>')
181
open('c', 'wb').write('just another manic monday\n')
183
wt.commit('single line with trailing newline\n', rev_id='a3'
184
, timestamp=1132587176.835228920, timezone=-6*3600
185
, committer = 'Joe Foo <joe@foo.com>')
188
lf = ShortLogFormatter(to_file=sio)
190
self.assertEquals(sio.getvalue(), """\
191
3 Joe Foo\t2005-11-21
192
single line with trailing newline
194
2 Joe Foo\t2005-11-21
199
1 Joe Foo\t2005-11-21
205
lf = LongLogFormatter(to_file=sio)
207
self.assertEquals(sio.getvalue(), """\
208
------------------------------------------------------------
210
committer: Joe Foo <joe@foo.com>
212
timestamp: Mon 2005-11-21 09:32:56 -0600
214
single line with trailing newline
215
------------------------------------------------------------
217
committer: Joe Foo <joe@foo.com>
219
timestamp: Mon 2005-11-21 09:27:22 -0600
224
------------------------------------------------------------
226
committer: Joe Foo <joe@foo.com>
228
timestamp: Mon 2005-11-21 09:24:15 -0600
233
def test_verbose_log(self):
234
"""Verbose log includes changed files
238
wt = self.make_branch_and_tree('.')
240
self.build_tree(['a'])
242
# XXX: why does a longer nick show up?
243
b.nick = 'test_verbose_log'
244
wt.commit(message='add a',
245
timestamp=1132711707,
247
committer='Lorem Ipsum <test@example.com>')
248
logfile = file('out.tmp', 'w+')
249
formatter = LongLogFormatter(to_file=logfile)
250
show_log(b, formatter, verbose=True)
253
log_contents = logfile.read()
254
self.assertEqualDiff(log_contents, '''\
255
------------------------------------------------------------
257
committer: Lorem Ipsum <test@example.com>
258
branch nick: test_verbose_log
259
timestamp: Wed 2005-11-23 12:08:27 +1000
266
def test_line_log(self):
267
"""Line log should show revno
271
wt = self.make_branch_and_tree('.')
273
self.build_tree(['a'])
275
b.nick = 'test-line-log'
276
wt.commit(message='add a',
277
timestamp=1132711707,
279
committer='Line-Log-Formatter Tester <test@line.log>')
280
logfile = file('out.tmp', 'w+')
281
formatter = LineLogFormatter(to_file=logfile)
282
show_log(b, formatter)
285
log_contents = logfile.read()
286
self.assertEqualDiff(log_contents, '1: Line-Log-Formatte... 2005-11-23 add a\n')
288
def make_tree_with_commits(self):
289
"""Create a tree with well-known revision ids"""
290
wt = self.make_branch_and_tree('tree1')
291
wt.commit('commit one', rev_id='1')
292
wt.commit('commit two', rev_id='2')
293
wt.commit('commit three', rev_id='3')
294
mainline_revs = [None, '1', '2', '3']
295
rev_nos = {'1': 1, '2': 2, '3': 3}
296
return mainline_revs, rev_nos, wt
298
def make_tree_with_merges(self):
299
"""Create a tree with well-known revision ids and a merge"""
300
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
301
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
302
tree2.commit('four-a', rev_id='4a')
303
wt.merge_from_branch(tree2.branch)
304
wt.commit('four-b', rev_id='4b')
305
mainline_revs.append('4b')
308
return mainline_revs, rev_nos, wt
310
def make_tree_with_many_merges(self):
311
"""Create a tree with well-known revision ids"""
312
wt = self.make_branch_and_tree('tree1')
313
wt.commit('commit one', rev_id='1')
314
wt.commit('commit two', rev_id='2')
315
tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
316
tree3.commit('commit three a', rev_id='3a')
317
tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
318
tree2.merge_from_branch(tree3.branch)
319
tree2.commit('commit three b', rev_id='3b')
320
wt.merge_from_branch(tree2.branch)
321
wt.commit('commit three c', rev_id='3c')
322
tree2.commit('four-a', rev_id='4a')
323
wt.merge_from_branch(tree2.branch)
324
wt.commit('four-b', rev_id='4b')
325
mainline_revs = [None, '1', '2', '3c', '4b']
326
rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
327
full_rev_nos_for_reference = {
330
'3a': '2.2.1', #first commit tree 3
331
'3b': '2.1.1', # first commit tree 2
332
'3c': '3', #merges 3b to main
333
'4a': '2.1.2', # second commit tree 2
334
'4b': '4', # merges 4a to main
336
return mainline_revs, rev_nos, wt
338
def test_get_view_revisions_forward(self):
339
"""Test the get_view_revisions method"""
340
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
341
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
343
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
345
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
346
'forward', include_merges=False))
347
self.assertEqual(revisions, revisions2)
349
def test_get_view_revisions_reverse(self):
350
"""Test the get_view_revisions with reverse"""
351
mainline_revs, rev_nos, wt = self.make_tree_with_commits()
352
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
354
self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
356
revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
357
'reverse', include_merges=False))
358
self.assertEqual(revisions, revisions2)
360
def test_get_view_revisions_merge(self):
361
"""Test get_view_revisions when there are merges"""
362
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
363
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
365
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
366
('4b', '4', 0), ('4a', '3.1.1', 1)],
368
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
369
'forward', include_merges=False))
370
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
374
def test_get_view_revisions_merge_reverse(self):
375
"""Test get_view_revisions in reverse when there are merges"""
376
mainline_revs, rev_nos, wt = self.make_tree_with_merges()
377
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
379
self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
380
('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
382
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
383
'reverse', include_merges=False))
384
self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
388
def test_get_view_revisions_merge2(self):
389
"""Test get_view_revisions when there are merges"""
390
mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
391
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
393
expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
394
('3a', '2.2.1', 1), ('3b', '2.1.1', 1), ('4b', '4', 0),
396
self.assertEqual(expected, revisions)
397
revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
398
'forward', include_merges=False))
399
self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
404
class TestGetRevisionsTouchingFileID(TestCaseWithTransport):
406
def create_tree_with_single_merge(self):
407
"""Create a branch with a moderate layout.
409
The revision graph looks like:
417
In this graph, A introduced files f1 and f2 and f3.
418
B modifies f1 and f3, and C modifies f2 and f3.
419
D merges the changes from B and C and resolves the conflict for f3.
421
# TODO: jam 20070218 This seems like it could really be done
422
# with make_branch_and_memory_tree() if we could just
423
# create the content of those files.
424
# TODO: jam 20070218 Another alternative is that we would really
425
# like to only create this tree 1 time for all tests that
426
# use it. Since 'log' only uses the tree in a readonly
427
# fashion, it seems a shame to regenerate an identical
428
# tree for each test.
429
tree = self.make_branch_and_tree('tree')
431
self.addCleanup(tree.unlock)
433
self.build_tree_contents([('tree/f1', 'A\n'),
437
tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
438
tree.commit('A', rev_id='A')
440
self.build_tree_contents([('tree/f2', 'A\nC\n'),
441
('tree/f3', 'A\nC\n'),
443
tree.commit('C', rev_id='C')
444
# Revert back to A to build the other history.
445
tree.set_last_revision('A')
446
tree.branch.set_last_revision_info(1, 'A')
447
self.build_tree_contents([('tree/f1', 'A\nB\n'),
449
('tree/f3', 'A\nB\n'),
451
tree.commit('B', rev_id='B')
452
tree.set_parent_ids(['B', 'C'])
453
self.build_tree_contents([('tree/f1', 'A\nB\n'),
454
('tree/f2', 'A\nC\n'),
455
('tree/f3', 'A\nB\nC\n'),
457
tree.commit('D', rev_id='D')
459
# Switch to a read lock for this tree.
460
# We still have addCleanup(unlock)
465
def test_tree_with_single_merge(self):
466
"""Make sure the tree layout is correct."""
467
tree = self.create_tree_with_single_merge()
468
rev_A_tree = tree.branch.repository.revision_tree('A')
469
rev_B_tree = tree.branch.repository.revision_tree('B')
471
f1_changed = (u'f1', 'f1-id', 'file', True, False)
472
f2_changed = (u'f2', 'f2-id', 'file', True, False)
473
f3_changed = (u'f3', 'f3-id', 'file', True, False)
475
delta = rev_B_tree.changes_from(rev_A_tree)
476
self.assertEqual([f1_changed, f3_changed], delta.modified)
477
self.assertEqual([], delta.renamed)
478
self.assertEqual([], delta.added)
479
self.assertEqual([], delta.removed)
481
rev_C_tree = tree.branch.repository.revision_tree('C')
482
delta = rev_C_tree.changes_from(rev_A_tree)
483
self.assertEqual([f2_changed, f3_changed], delta.modified)
484
self.assertEqual([], delta.renamed)
485
self.assertEqual([], delta.added)
486
self.assertEqual([], delta.removed)
488
rev_D_tree = tree.branch.repository.revision_tree('D')
489
delta = rev_D_tree.changes_from(rev_B_tree)
490
self.assertEqual([f2_changed, f3_changed], delta.modified)
491
self.assertEqual([], delta.renamed)
492
self.assertEqual([], delta.added)
493
self.assertEqual([], delta.removed)
495
delta = rev_D_tree.changes_from(rev_C_tree)
496
self.assertEqual([f1_changed, f3_changed], delta.modified)
497
self.assertEqual([], delta.renamed)
498
self.assertEqual([], delta.added)
499
self.assertEqual([], delta.removed)
501
def assertAllRevisionsForFileID(self, tree, file_id, revisions):
502
"""Make sure _get_revisions_touching_file_id returns the right values.
504
Get the return value from _get_revisions_touching_file_id and make
505
sure they are correct.
507
# The api for _get_revisions_touching_file_id is a little crazy,
508
# So we do the setup here.
509
mainline = tree.branch.revision_history()
510
mainline.insert(0, None)
511
revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
512
view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
514
actual_revs = log._get_revisions_touching_file_id(tree.branch, file_id,
517
self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
519
def test_file_id_f1(self):
520
tree = self.create_tree_with_single_merge()
521
# f1 should be marked as modified by revisions A and B
522
self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
524
def test_file_id_f2(self):
525
tree = self.create_tree_with_single_merge()
526
# f2 should be marked as modified by revisions A, C, and D
527
# because D merged the changes from C.
528
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
530
def test_file_id_f3(self):
531
tree = self.create_tree_with_single_merge()
532
# f3 should be marked as modified by revisions A, B, C, and D
533
self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])