/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_log.py

  • Committer: Vincent Ladeuil
  • Date: 2009-01-23 21:06:48 UTC
  • mto: (3966.1.1 bzr.integration)
  • mto: This revision was merged to the branch mainline in revision 3967.
  • Revision ID: v.ladeuil+lp@free.fr-20090123210648-yfb39g22yyo83d3y
Slight refactoring and test fixing.

* bzrlib/tests/test_merge.py:
(TestMergerEntriesLCAOnDisk.test_modified_symlink): Passing now.

* bzrlib/merge.py:
(Merge3Merger._lca_multi_way): Fix doc reference.
(Merge3Merger.merge_contents.contents_conflict): Try to delay
this_pair evaulation to avoid unnecessary sha1 (impyling file read
from disk) calculation. Also slightly refactor to avoid repeated
file_id in trees calculations.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
import os
 
18
from cStringIO import StringIO
 
19
 
 
20
from bzrlib import (
 
21
    errors,
 
22
    log,
 
23
    registry,
 
24
    revision,
 
25
    revisionspec,
 
26
    tests,
 
27
    )
 
28
 
 
29
 
 
30
class TestCaseWithoutPropsHandler(tests.TestCaseWithTransport):
 
31
 
 
32
    def setUp(self):
 
33
        super(TestCaseWithoutPropsHandler, self).setUp()
 
34
        # keep a reference to the "current" custom prop. handler registry
 
35
        self.properties_handler_registry = log.properties_handler_registry
 
36
        # clean up the registry in log
 
37
        log.properties_handler_registry = registry.Registry()
 
38
 
 
39
    def _cleanup(self):
 
40
        super(TestCaseWithoutPropsHandler, self)._cleanup()
 
41
        # restore the custom properties handler registry
 
42
        log.properties_handler_registry = self.properties_handler_registry
 
43
 
 
44
 
 
45
class LogCatcher(log.LogFormatter):
 
46
    """Pull log messages into list rather than displaying them.
 
47
 
 
48
    For ease of testing we save log messages here rather than actually
 
49
    formatting them, so that we can precisely check the result without
 
50
    being too dependent on the exact formatting.
 
51
 
 
52
    We should also test the LogFormatter.
 
53
    """
 
54
 
 
55
    supports_delta = True
 
56
 
 
57
    def __init__(self):
 
58
        super(LogCatcher, self).__init__(to_file=None)
 
59
        self.logs = []
 
60
 
 
61
    def log_revision(self, revision):
 
62
        self.logs.append(revision)
 
63
 
 
64
 
 
65
class TestShowLog(tests.TestCaseWithTransport):
 
66
 
 
67
    def checkDelta(self, delta, **kw):
 
68
        """Check the filenames touched by a delta are as expected.
 
69
 
 
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).
 
72
        """
 
73
        for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
 
74
            # By default we expect an empty list
 
75
            expected = kw.get(n, [])
 
76
            # strip out only the path components
 
77
            got = [x[0] for x in getattr(delta, n)]
 
78
            self.assertEqual(expected, got)
 
79
 
 
80
    def assertInvalidRevisonNumber(self, br, start, end):
 
81
        lf = LogCatcher()
 
82
        self.assertRaises(errors.InvalidRevisionNumber,
 
83
                          log.show_log, br, lf,
 
84
                          start_revision=start, end_revision=end)
 
85
 
 
86
    def test_cur_revno(self):
 
87
        wt = self.make_branch_and_tree('.')
 
88
        b = wt.branch
 
89
 
 
90
        lf = LogCatcher()
 
91
        wt.commit('empty commit')
 
92
        log.show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
 
93
 
 
94
        # Since there is a single revision in the branch all the combinations
 
95
        # below should fail.
 
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)
 
102
 
 
103
    def test_empty_branch(self):
 
104
        wt = self.make_branch_and_tree('.')
 
105
 
 
106
        lf = LogCatcher()
 
107
        log.show_log(wt.branch, lf)
 
108
        # no entries yet
 
109
        self.assertEqual([], lf.logs)
 
110
 
 
111
    def test_empty_commit(self):
 
112
        wt = self.make_branch_and_tree('.')
 
113
 
 
114
        wt.commit('empty commit')
 
115
        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)
 
121
 
 
122
    def test_simple_commit(self):
 
123
        wt = self.make_branch_and_tree('.')
 
124
        wt.commit('empty commit')
 
125
        self.build_tree(['hello'])
 
126
        wt.add('hello')
 
127
        wt.commit('add one file',
 
128
                  committer=u'\u013d\xf3r\xe9m \xcdp\u0161\xfam '
 
129
                            u'<test@example.com>')
 
130
        lf = LogCatcher()
 
131
        log.show_log(wt.branch, lf, verbose=True)
 
132
        self.assertEqual(2, len(lf.logs))
 
133
        # 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'])
 
138
 
 
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')
 
143
        wt.commit(msg)
 
144
        lf = LogCatcher()
 
145
        log.show_log(wt.branch, lf, verbose=True)
 
146
        committed_msg = lf.logs[0].rev.message
 
147
        self.assertNotEqual(msg, committed_msg)
 
148
        self.assertTrue(len(committed_msg) > len(msg))
 
149
 
 
150
    def test_commit_message_without_control_chars(self):
 
151
        wt = self.make_branch_and_tree('.')
 
152
        # escaped.  As ElementTree apparently does some kind of
 
153
        # newline conversion, neither LF (\x0A) nor CR (\x0D) are
 
154
        # included in the test commit message, even though they are
 
155
        # valid XML 1.0 characters.
 
156
        msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
 
157
        wt.commit(msg)
 
158
        lf = LogCatcher()
 
159
        log.show_log(wt.branch, lf, verbose=True)
 
160
        committed_msg = lf.logs[0].rev.message
 
161
        self.assertEqual(msg, committed_msg)
 
162
 
 
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'])
 
167
        wt.add('file1')
 
168
        wt.add('file2')
 
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',
 
174
            'child'])
 
175
        os.chdir('parent')
 
176
        self.run_bzr('merge ../child')
 
177
        wt.commit('merge child branch')
 
178
        os.chdir('..')
 
179
        b = wt.branch
 
180
        lf = LogCatcher()
 
181
        lf.supports_merge_revisions = True
 
182
        log.show_log(b, lf, verbose=True)
 
183
 
 
184
        self.assertEqual(3, len(lf.logs))
 
185
 
 
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'])
 
190
 
 
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'])
 
195
 
 
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'])
 
200
 
 
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('.')
 
205
        wt.lock_write()
 
206
        self.addCleanup(wt.unlock)
 
207
        wt.add('')
 
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)
 
221
        wtb = wt.branch
 
222
        lf = LogCatcher()
 
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)
 
227
 
 
228
 
 
229
def make_commits_with_trailing_newlines(wt):
 
230
    """Helper method for LogFormatter tests"""
 
231
    b = wt.branch
 
232
    b.nick='test'
 
233
    open('a', 'wb').write('hello moto\n')
 
234
    wt.add('a')
 
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')
 
239
    wt.add('b')
 
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>')
 
244
 
 
245
    open('c', 'wb').write('just another manic monday\n')
 
246
    wt.add('c')
 
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>')
 
250
    return b
 
251
 
 
252
 
 
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)
 
268
 
 
269
 
 
270
class TestShortLogFormatter(tests.TestCaseWithTransport):
 
271
 
 
272
    def test_trailing_newlines(self):
 
273
        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)
 
277
        log.show_log(b, lf)
 
278
        self.assertEqualDiff("""\
 
279
    3 Joe Foo\t2005-11-21
 
280
      single line with trailing newline
 
281
 
 
282
    2 Joe Bar\t2005-11-21
 
283
      multiline
 
284
      log
 
285
      message
 
286
 
 
287
    1 Joe Foo\t2005-11-21
 
288
      simple log message
 
289
 
 
290
""",
 
291
                             sio.getvalue())
 
292
 
 
293
    def _prepare_tree_with_merges(self, with_tags=False):
 
294
        wt = self.make_branch_and_memory_tree('.')
 
295
        wt.lock_write()
 
296
        self.addCleanup(wt.unlock)
 
297
        wt.add('')
 
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
        if with_tags:
 
310
            branch = wt.branch
 
311
            branch.tags.set_tag('v0.2', 'rev-2b')
 
312
            wt.commit('rev-3', rev_id='rev-3',
 
313
                      timestamp=1132586900, timezone=36000,
 
314
                      committer='Jane Foo <jane@foo.com>')
 
315
            branch.tags.set_tag('v1.0rc1', 'rev-3')
 
316
            branch.tags.set_tag('v1.0', 'rev-3')
 
317
        return wt
 
318
 
 
319
    def test_short_log_with_merges(self):
 
320
        wt = self._prepare_tree_with_merges()
 
321
        logfile = self.make_utf8_encoded_stringio()
 
322
        formatter = log.ShortLogFormatter(to_file=logfile)
 
323
        log.show_log(wt.branch, formatter)
 
324
        self.assertEqualDiff("""\
 
325
    2 Joe Foo\t2005-11-22 [merge]
 
326
      rev-2
 
327
 
 
328
    1 Joe Foo\t2005-11-22
 
329
      rev-1
 
330
 
 
331
""",
 
332
                             logfile.getvalue())
 
333
 
 
334
    def test_short_log_with_merges_and_range(self):
 
335
        wt = self.make_branch_and_memory_tree('.')
 
336
        wt.lock_write()
 
337
        self.addCleanup(wt.unlock)
 
338
        wt.add('')
 
339
        wt.commit('rev-1', rev_id='rev-1',
 
340
                  timestamp=1132586655, timezone=36000,
 
341
                  committer='Joe Foo <joe@foo.com>')
 
342
        wt.commit('rev-merged', rev_id='rev-2a',
 
343
                  timestamp=1132586700, timezone=36000,
 
344
                  committer='Joe Foo <joe@foo.com>')
 
345
        wt.branch.set_last_revision_info(1, 'rev-1')
 
346
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
347
        wt.commit('rev-2b', rev_id='rev-2b',
 
348
                  timestamp=1132586800, timezone=36000,
 
349
                  committer='Joe Foo <joe@foo.com>')
 
350
        wt.commit('rev-3a', rev_id='rev-3a',
 
351
                  timestamp=1132586800, timezone=36000,
 
352
                  committer='Joe Foo <joe@foo.com>')
 
353
        wt.branch.set_last_revision_info(2, 'rev-2b')
 
354
        wt.set_parent_ids(['rev-2b', 'rev-3a'])
 
355
        wt.commit('rev-3b', rev_id='rev-3b',
 
356
                  timestamp=1132586800, timezone=36000,
 
357
                  committer='Joe Foo <joe@foo.com>')
 
358
        logfile = self.make_utf8_encoded_stringio()
 
359
        formatter = log.ShortLogFormatter(to_file=logfile)
 
360
        log.show_log(wt.branch, formatter,
 
361
            start_revision=2, end_revision=3)
 
362
        self.assertEqualDiff("""\
 
363
    3 Joe Foo\t2005-11-22 [merge]
 
364
      rev-3b
 
365
 
 
366
    2 Joe Foo\t2005-11-22 [merge]
 
367
      rev-2b
 
368
 
 
369
""",
 
370
                             logfile.getvalue())
 
371
 
 
372
    def test_short_log_with_tags(self):
 
373
        wt = self._prepare_tree_with_merges(with_tags=True)
 
374
        logfile = self.make_utf8_encoded_stringio()
 
375
        formatter = log.ShortLogFormatter(to_file=logfile)
 
376
        log.show_log(wt.branch, formatter)
 
377
        self.assertEqualDiff("""\
 
378
    3 Jane Foo\t2005-11-22 {v1.0, v1.0rc1}
 
379
      rev-3
 
380
 
 
381
    2 Joe Foo\t2005-11-22 {v0.2} [merge]
 
382
      rev-2
 
383
 
 
384
    1 Joe Foo\t2005-11-22
 
385
      rev-1
 
386
 
 
387
""",
 
388
                             logfile.getvalue())
 
389
 
 
390
    def test_short_log_single_merge_revision(self):
 
391
        wt = self.make_branch_and_memory_tree('.')
 
392
        wt.lock_write()
 
393
        self.addCleanup(wt.unlock)
 
394
        wt.add('')
 
395
        wt.commit('rev-1', rev_id='rev-1',
 
396
                  timestamp=1132586655, timezone=36000,
 
397
                  committer='Joe Foo <joe@foo.com>')
 
398
        wt.commit('rev-merged', rev_id='rev-2a',
 
399
                  timestamp=1132586700, timezone=36000,
 
400
                  committer='Joe Foo <joe@foo.com>')
 
401
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
402
        wt.branch.set_last_revision_info(1, 'rev-1')
 
403
        wt.commit('rev-2', rev_id='rev-2b',
 
404
                  timestamp=1132586800, timezone=36000,
 
405
                  committer='Joe Foo <joe@foo.com>')
 
406
        logfile = self.make_utf8_encoded_stringio()
 
407
        formatter = log.ShortLogFormatter(to_file=logfile)
 
408
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
 
409
        wtb = wt.branch
 
410
        rev = revspec.in_history(wtb)
 
411
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
412
        self.assertEqualDiff("""\
 
413
1.1.1 Joe Foo\t2005-11-22
 
414
      rev-merged
 
415
 
 
416
""",
 
417
                             logfile.getvalue())
 
418
 
 
419
 
 
420
class TestLongLogFormatter(TestCaseWithoutPropsHandler):
 
421
 
 
422
    def test_verbose_log(self):
 
423
        """Verbose log includes changed files
 
424
        
 
425
        bug #4676
 
426
        """
 
427
        wt = self.make_branch_and_tree('.')
 
428
        b = wt.branch
 
429
        self.build_tree(['a'])
 
430
        wt.add('a')
 
431
        # XXX: why does a longer nick show up?
 
432
        b.nick = 'test_verbose_log'
 
433
        wt.commit(message='add a',
 
434
                  timestamp=1132711707,
 
435
                  timezone=36000,
 
436
                  committer='Lorem Ipsum <test@example.com>')
 
437
        logfile = file('out.tmp', 'w+')
 
438
        formatter = log.LongLogFormatter(to_file=logfile)
 
439
        log.show_log(b, formatter, verbose=True)
 
440
        logfile.flush()
 
441
        logfile.seek(0)
 
442
        log_contents = logfile.read()
 
443
        self.assertEqualDiff('''\
 
444
------------------------------------------------------------
 
445
revno: 1
 
446
committer: Lorem Ipsum <test@example.com>
 
447
branch nick: test_verbose_log
 
448
timestamp: Wed 2005-11-23 12:08:27 +1000
 
449
message:
 
450
  add a
 
451
added:
 
452
  a
 
453
''',
 
454
                             log_contents)
 
455
 
 
456
    def test_merges_are_indented_by_level(self):
 
457
        wt = self.make_branch_and_tree('parent')
 
458
        wt.commit('first post')
 
459
        self.run_bzr('branch parent child')
 
460
        self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
 
461
        self.run_bzr('branch child smallerchild')
 
462
        self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
 
463
            'smallerchild'])
 
464
        os.chdir('child')
 
465
        self.run_bzr('merge ../smallerchild')
 
466
        self.run_bzr(['commit', '-m', 'merge branch 2'])
 
467
        os.chdir('../parent')
 
468
        self.run_bzr('merge ../child')
 
469
        wt.commit('merge branch 1')
 
470
        b = wt.branch
 
471
        sio = self.make_utf8_encoded_stringio()
 
472
        lf = log.LongLogFormatter(to_file=sio)
 
473
        log.show_log(b, lf, verbose=True)
 
474
        the_log = normalize_log(sio.getvalue())
 
475
        self.assertEqualDiff("""\
 
476
------------------------------------------------------------
 
477
revno: 2
 
478
committer: Lorem Ipsum <test@example.com>
 
479
branch nick: parent
 
480
timestamp: Just now
 
481
message:
 
482
  merge branch 1
 
483
    ------------------------------------------------------------
 
484
    revno: 1.1.2
 
485
    committer: Lorem Ipsum <test@example.com>
 
486
    branch nick: child
 
487
    timestamp: Just now
 
488
    message:
 
489
      merge branch 2
 
490
        ------------------------------------------------------------
 
491
        revno: 1.2.1
 
492
        committer: Lorem Ipsum <test@example.com>
 
493
        branch nick: smallerchild
 
494
        timestamp: Just now
 
495
        message:
 
496
          branch 2
 
497
    ------------------------------------------------------------
 
498
    revno: 1.1.1
 
499
    committer: Lorem Ipsum <test@example.com>
 
500
    branch nick: child
 
501
    timestamp: Just now
 
502
    message:
 
503
      branch 1
 
504
------------------------------------------------------------
 
505
revno: 1
 
506
committer: Lorem Ipsum <test@example.com>
 
507
branch nick: parent
 
508
timestamp: Just now
 
509
message:
 
510
  first post
 
511
""",
 
512
                             the_log)
 
513
 
 
514
    def test_verbose_merge_revisions_contain_deltas(self):
 
515
        wt = self.make_branch_and_tree('parent')
 
516
        self.build_tree(['parent/f1', 'parent/f2'])
 
517
        wt.add(['f1','f2'])
 
518
        wt.commit('first post')
 
519
        self.run_bzr('branch parent child')
 
520
        os.unlink('child/f1')
 
521
        file('child/f2', 'wb').write('hello\n')
 
522
        self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
 
523
            'child'])
 
524
        os.chdir('parent')
 
525
        self.run_bzr('merge ../child')
 
526
        wt.commit('merge branch 1')
 
527
        b = wt.branch
 
528
        sio = self.make_utf8_encoded_stringio()
 
529
        lf = log.LongLogFormatter(to_file=sio)
 
530
        log.show_log(b, lf, verbose=True)
 
531
        the_log = normalize_log(sio.getvalue())
 
532
        self.assertEqualDiff("""\
 
533
------------------------------------------------------------
 
534
revno: 2
 
535
committer: Lorem Ipsum <test@example.com>
 
536
branch nick: parent
 
537
timestamp: Just now
 
538
message:
 
539
  merge branch 1
 
540
removed:
 
541
  f1
 
542
modified:
 
543
  f2
 
544
    ------------------------------------------------------------
 
545
    revno: 1.1.1
 
546
    committer: Lorem Ipsum <test@example.com>
 
547
    branch nick: child
 
548
    timestamp: Just now
 
549
    message:
 
550
      removed f1 and modified f2
 
551
    removed:
 
552
      f1
 
553
    modified:
 
554
      f2
 
555
------------------------------------------------------------
 
556
revno: 1
 
557
committer: Lorem Ipsum <test@example.com>
 
558
branch nick: parent
 
559
timestamp: Just now
 
560
message:
 
561
  first post
 
562
added:
 
563
  f1
 
564
  f2
 
565
""",
 
566
                             the_log)
 
567
 
 
568
    def test_trailing_newlines(self):
 
569
        wt = self.make_branch_and_tree('.')
 
570
        b = make_commits_with_trailing_newlines(wt)
 
571
        sio = self.make_utf8_encoded_stringio()
 
572
        lf = log.LongLogFormatter(to_file=sio)
 
573
        log.show_log(b, lf)
 
574
        self.assertEqualDiff("""\
 
575
------------------------------------------------------------
 
576
revno: 3
 
577
committer: Joe Foo <joe@foo.com>
 
578
branch nick: test
 
579
timestamp: Mon 2005-11-21 09:32:56 -0600
 
580
message:
 
581
  single line with trailing newline
 
582
------------------------------------------------------------
 
583
revno: 2
 
584
author: Joe Bar <joe@bar.com>
 
585
committer: Joe Foo <joe@foo.com>
 
586
branch nick: test
 
587
timestamp: Mon 2005-11-21 09:27:22 -0600
 
588
message:
 
589
  multiline
 
590
  log
 
591
  message
 
592
------------------------------------------------------------
 
593
revno: 1
 
594
committer: Joe Foo <joe@foo.com>
 
595
branch nick: test
 
596
timestamp: Mon 2005-11-21 09:24:15 -0600
 
597
message:
 
598
  simple log message
 
599
""",
 
600
                             sio.getvalue())
 
601
 
 
602
    def test_author_in_log(self):
 
603
        """Log includes the author name if it's set in
 
604
        the revision properties
 
605
        """
 
606
        wt = self.make_branch_and_tree('.')
 
607
        b = wt.branch
 
608
        self.build_tree(['a'])
 
609
        wt.add('a')
 
610
        b.nick = 'test_author_log'
 
611
        wt.commit(message='add a',
 
612
                  timestamp=1132711707,
 
613
                  timezone=36000,
 
614
                  committer='Lorem Ipsum <test@example.com>',
 
615
                  author='John Doe <jdoe@example.com>')
 
616
        sio = StringIO()
 
617
        formatter = log.LongLogFormatter(to_file=sio)
 
618
        log.show_log(b, formatter)
 
619
        self.assertEqualDiff('''\
 
620
------------------------------------------------------------
 
621
revno: 1
 
622
author: John Doe <jdoe@example.com>
 
623
committer: Lorem Ipsum <test@example.com>
 
624
branch nick: test_author_log
 
625
timestamp: Wed 2005-11-23 12:08:27 +1000
 
626
message:
 
627
  add a
 
628
''',
 
629
                             sio.getvalue())
 
630
 
 
631
    def test_properties_in_log(self):
 
632
        """Log includes the custom properties returned by the registered 
 
633
        handlers.
 
634
        """
 
635
        wt = self.make_branch_and_tree('.')
 
636
        b = wt.branch
 
637
        self.build_tree(['a'])
 
638
        wt.add('a')
 
639
        b.nick = 'test_properties_in_log'
 
640
        wt.commit(message='add a',
 
641
                  timestamp=1132711707,
 
642
                  timezone=36000,
 
643
                  committer='Lorem Ipsum <test@example.com>',
 
644
                  author='John Doe <jdoe@example.com>')
 
645
        sio = StringIO()
 
646
        formatter = log.LongLogFormatter(to_file=sio)
 
647
        try:
 
648
            def trivial_custom_prop_handler(revision):
 
649
                return {'test_prop':'test_value'}
 
650
 
 
651
            log.properties_handler_registry.register(
 
652
                'trivial_custom_prop_handler',
 
653
                trivial_custom_prop_handler)
 
654
            log.show_log(b, formatter)
 
655
        finally:
 
656
            log.properties_handler_registry.remove(
 
657
                'trivial_custom_prop_handler')
 
658
            self.assertEqualDiff('''\
 
659
------------------------------------------------------------
 
660
revno: 1
 
661
test_prop: test_value
 
662
author: John Doe <jdoe@example.com>
 
663
committer: Lorem Ipsum <test@example.com>
 
664
branch nick: test_properties_in_log
 
665
timestamp: Wed 2005-11-23 12:08:27 +1000
 
666
message:
 
667
  add a
 
668
''',
 
669
                                 sio.getvalue())
 
670
 
 
671
    def test_error_in_properties_handler(self):
 
672
        """Log includes the custom properties returned by the registered 
 
673
        handlers.
 
674
        """
 
675
        wt = self.make_branch_and_tree('.')
 
676
        b = wt.branch
 
677
        self.build_tree(['a'])
 
678
        wt.add('a')
 
679
        b.nick = 'test_author_log'
 
680
        wt.commit(message='add a',
 
681
                  timestamp=1132711707,
 
682
                  timezone=36000,
 
683
                  committer='Lorem Ipsum <test@example.com>',
 
684
                  author='John Doe <jdoe@example.com>',
 
685
                  revprops={'first_prop':'first_value'})
 
686
        sio = StringIO()
 
687
        formatter = log.LongLogFormatter(to_file=sio)
 
688
        try:
 
689
            def trivial_custom_prop_handler(revision):
 
690
                raise StandardError("a test error")
 
691
 
 
692
            log.properties_handler_registry.register(
 
693
                'trivial_custom_prop_handler',
 
694
                trivial_custom_prop_handler)
 
695
            self.assertRaises(StandardError, log.show_log, b, formatter,)
 
696
        finally:
 
697
            log.properties_handler_registry.remove(
 
698
                'trivial_custom_prop_handler')
 
699
 
 
700
    def test_properties_handler_bad_argument(self):
 
701
        wt = self.make_branch_and_tree('.')
 
702
        b = wt.branch
 
703
        self.build_tree(['a'])
 
704
        wt.add('a')
 
705
        b.nick = 'test_author_log'
 
706
        wt.commit(message='add a',
 
707
                  timestamp=1132711707,
 
708
                  timezone=36000,
 
709
                  committer='Lorem Ipsum <test@example.com>',
 
710
                  author='John Doe <jdoe@example.com>',
 
711
                  revprops={'a_prop':'test_value'})
 
712
        sio = StringIO()
 
713
        formatter = log.LongLogFormatter(to_file=sio)
 
714
        try:
 
715
            def bad_argument_prop_handler(revision):
 
716
                return {'custom_prop_name':revision.properties['a_prop']}
 
717
 
 
718
            log.properties_handler_registry.register(
 
719
                'bad_argument_prop_handler',
 
720
                bad_argument_prop_handler)
 
721
 
 
722
            self.assertRaises(AttributeError, formatter.show_properties,
 
723
                              'a revision', '')
 
724
 
 
725
            revision = b.repository.get_revision(b.last_revision())
 
726
            formatter.show_properties(revision, '')
 
727
            self.assertEqualDiff('''custom_prop_name: test_value\n''',
 
728
                                 sio.getvalue())
 
729
        finally:
 
730
            log.properties_handler_registry.remove(
 
731
                'bad_argument_prop_handler')
 
732
 
 
733
 
 
734
class TestLineLogFormatter(tests.TestCaseWithTransport):
 
735
 
 
736
    def test_line_log(self):
 
737
        """Line log should show revno
 
738
        
 
739
        bug #5162
 
740
        """
 
741
        wt = self.make_branch_and_tree('.')
 
742
        b = wt.branch
 
743
        self.build_tree(['a'])
 
744
        wt.add('a')
 
745
        b.nick = 'test-line-log'
 
746
        wt.commit(message='add a',
 
747
                  timestamp=1132711707,
 
748
                  timezone=36000,
 
749
                  committer='Line-Log-Formatter Tester <test@line.log>')
 
750
        logfile = file('out.tmp', 'w+')
 
751
        formatter = log.LineLogFormatter(to_file=logfile)
 
752
        log.show_log(b, formatter)
 
753
        logfile.flush()
 
754
        logfile.seek(0)
 
755
        log_contents = logfile.read()
 
756
        self.assertEqualDiff('1: Line-Log-Formatte... 2005-11-23 add a\n',
 
757
                             log_contents)
 
758
 
 
759
    def test_trailing_newlines(self):
 
760
        wt = self.make_branch_and_tree('.')
 
761
        b = make_commits_with_trailing_newlines(wt)
 
762
        sio = self.make_utf8_encoded_stringio()
 
763
        lf = log.LineLogFormatter(to_file=sio)
 
764
        log.show_log(b, lf)
 
765
        self.assertEqualDiff("""\
 
766
3: Joe Foo 2005-11-21 single line with trailing newline
 
767
2: Joe Bar 2005-11-21 multiline
 
768
1: Joe Foo 2005-11-21 simple log message
 
769
""",
 
770
                             sio.getvalue())
 
771
 
 
772
    def _prepare_tree_with_merges(self, with_tags=False):
 
773
        wt = self.make_branch_and_memory_tree('.')
 
774
        wt.lock_write()
 
775
        self.addCleanup(wt.unlock)
 
776
        wt.add('')
 
777
        wt.commit('rev-1', rev_id='rev-1',
 
778
                  timestamp=1132586655, timezone=36000,
 
779
                  committer='Joe Foo <joe@foo.com>')
 
780
        wt.commit('rev-merged', rev_id='rev-2a',
 
781
                  timestamp=1132586700, timezone=36000,
 
782
                  committer='Joe Foo <joe@foo.com>')
 
783
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
784
        wt.branch.set_last_revision_info(1, 'rev-1')
 
785
        wt.commit('rev-2', rev_id='rev-2b',
 
786
                  timestamp=1132586800, timezone=36000,
 
787
                  committer='Joe Foo <joe@foo.com>')
 
788
        if with_tags:
 
789
            branch = wt.branch
 
790
            branch.tags.set_tag('v0.2', 'rev-2b')
 
791
            wt.commit('rev-3', rev_id='rev-3',
 
792
                      timestamp=1132586900, timezone=36000,
 
793
                      committer='Jane Foo <jane@foo.com>')
 
794
            branch.tags.set_tag('v1.0rc1', 'rev-3')
 
795
            branch.tags.set_tag('v1.0', 'rev-3')
 
796
        return wt
 
797
 
 
798
    def test_line_log_single_merge_revision(self):
 
799
        wt = self._prepare_tree_with_merges()
 
800
        logfile = self.make_utf8_encoded_stringio()
 
801
        formatter = log.LineLogFormatter(to_file=logfile)
 
802
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
 
803
        wtb = wt.branch
 
804
        rev = revspec.in_history(wtb)
 
805
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
806
        self.assertEqualDiff("""\
 
807
1.1.1: Joe Foo 2005-11-22 rev-merged
 
808
""",
 
809
                             logfile.getvalue())
 
810
 
 
811
    def test_line_log_with_tags(self):
 
812
        wt = self._prepare_tree_with_merges(with_tags=True)
 
813
        logfile = self.make_utf8_encoded_stringio()
 
814
        formatter = log.LineLogFormatter(to_file=logfile)
 
815
        log.show_log(wt.branch, formatter)
 
816
        self.assertEqualDiff("""\
 
817
3: Jane Foo 2005-11-22 {v1.0, v1.0rc1} rev-3
 
818
2: Joe Foo 2005-11-22 {v0.2} rev-2
 
819
1: Joe Foo 2005-11-22 rev-1
 
820
""",
 
821
                             logfile.getvalue())
 
822
 
 
823
 
 
824
class TestGetViewRevisions(tests.TestCaseWithTransport):
 
825
 
 
826
    def make_tree_with_commits(self):
 
827
        """Create a tree with well-known revision ids"""
 
828
        wt = self.make_branch_and_tree('tree1')
 
829
        wt.commit('commit one', rev_id='1')
 
830
        wt.commit('commit two', rev_id='2')
 
831
        wt.commit('commit three', rev_id='3')
 
832
        mainline_revs = [None, '1', '2', '3']
 
833
        rev_nos = {'1': 1, '2': 2, '3': 3}
 
834
        return mainline_revs, rev_nos, wt
 
835
 
 
836
    def make_tree_with_merges(self):
 
837
        """Create a tree with well-known revision ids and a merge"""
 
838
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
 
839
        tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
 
840
        tree2.commit('four-a', rev_id='4a')
 
841
        wt.merge_from_branch(tree2.branch)
 
842
        wt.commit('four-b', rev_id='4b')
 
843
        mainline_revs.append('4b')
 
844
        rev_nos['4b'] = 4
 
845
        # 4a: 3.1.1
 
846
        return mainline_revs, rev_nos, wt
 
847
 
 
848
    def make_tree_with_many_merges(self):
 
849
        """Create a tree with well-known revision ids"""
 
850
        wt = self.make_branch_and_tree('tree1')
 
851
        self.build_tree_contents([('tree1/f', '1\n')])
 
852
        wt.add(['f'], ['f-id'])
 
853
        wt.commit('commit one', rev_id='1')
 
854
        wt.commit('commit two', rev_id='2')
 
855
 
 
856
        tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
 
857
        self.build_tree_contents([('tree3/f', '1\n2\n3a\n')])
 
858
        tree3.commit('commit three a', rev_id='3a')
 
859
 
 
860
        tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
 
861
        tree2.merge_from_branch(tree3.branch)
 
862
        tree2.commit('commit three b', rev_id='3b')
 
863
 
 
864
        wt.merge_from_branch(tree2.branch)
 
865
        wt.commit('commit three c', rev_id='3c')
 
866
        tree2.commit('four-a', rev_id='4a')
 
867
 
 
868
        wt.merge_from_branch(tree2.branch)
 
869
        wt.commit('four-b', rev_id='4b')
 
870
 
 
871
        mainline_revs = [None, '1', '2', '3c', '4b']
 
872
        rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
 
873
        full_rev_nos_for_reference = {
 
874
            '1': '1',
 
875
            '2': '2',
 
876
            '3a': '2.1.1', #first commit tree 3
 
877
            '3b': '2.2.1', # first commit tree 2
 
878
            '3c': '3', #merges 3b to main
 
879
            '4a': '2.2.2', # second commit tree 2
 
880
            '4b': '4', # merges 4a to main
 
881
            }
 
882
        return mainline_revs, rev_nos, wt
 
883
 
 
884
    def test_get_view_revisions_forward(self):
 
885
        """Test the get_view_revisions method"""
 
886
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
 
887
        wt.lock_read()
 
888
        self.addCleanup(wt.unlock)
 
889
        revisions = list(log.get_view_revisions(
 
890
                mainline_revs, rev_nos, wt.branch, 'forward'))
 
891
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
 
892
                         revisions)
 
893
        revisions2 = list(log.get_view_revisions(
 
894
                mainline_revs, rev_nos, wt.branch, 'forward',
 
895
                include_merges=False))
 
896
        self.assertEqual(revisions, revisions2)
 
897
 
 
898
    def test_get_view_revisions_reverse(self):
 
899
        """Test the get_view_revisions with reverse"""
 
900
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
 
901
        wt.lock_read()
 
902
        self.addCleanup(wt.unlock)
 
903
        revisions = list(log.get_view_revisions(
 
904
                mainline_revs, rev_nos, wt.branch, 'reverse'))
 
905
        self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
 
906
                         revisions)
 
907
        revisions2 = list(log.get_view_revisions(
 
908
                mainline_revs, rev_nos, wt.branch, 'reverse',
 
909
                include_merges=False))
 
910
        self.assertEqual(revisions, revisions2)
 
911
 
 
912
    def test_get_view_revisions_merge(self):
 
913
        """Test get_view_revisions when there are merges"""
 
914
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
 
915
        wt.lock_read()
 
916
        self.addCleanup(wt.unlock)
 
917
        revisions = list(log.get_view_revisions(
 
918
                mainline_revs, rev_nos, wt.branch, 'forward'))
 
919
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
 
920
                          ('4b', '4', 0), ('4a', '3.1.1', 1)],
 
921
                         revisions)
 
922
        revisions = list(log.get_view_revisions(
 
923
                mainline_revs, rev_nos, wt.branch, 'forward',
 
924
                include_merges=False))
 
925
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
 
926
                          ('4b', '4', 0)],
 
927
                         revisions)
 
928
 
 
929
    def test_get_view_revisions_merge_reverse(self):
 
930
        """Test get_view_revisions in reverse when there are merges"""
 
931
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
 
932
        wt.lock_read()
 
933
        self.addCleanup(wt.unlock)
 
934
        revisions = list(log.get_view_revisions(
 
935
                mainline_revs, rev_nos, wt.branch, 'reverse'))
 
936
        self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
 
937
                          ('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
 
938
                         revisions)
 
939
        revisions = list(log.get_view_revisions(
 
940
                mainline_revs, rev_nos, wt.branch, 'reverse',
 
941
                include_merges=False))
 
942
        self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
 
943
                          ('1', '1', 0)],
 
944
                         revisions)
 
945
 
 
946
    def test_get_view_revisions_merge2(self):
 
947
        """Test get_view_revisions when there are merges"""
 
948
        mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
 
949
        wt.lock_read()
 
950
        self.addCleanup(wt.unlock)
 
951
        revisions = list(log.get_view_revisions(
 
952
                mainline_revs, rev_nos, wt.branch, 'forward'))
 
953
        expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
 
954
                    ('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
 
955
                    ('4a', '2.2.2', 1)]
 
956
        self.assertEqual(expected, revisions)
 
957
        revisions = list(log.get_view_revisions(
 
958
                mainline_revs, rev_nos, wt.branch, 'forward',
 
959
                include_merges=False))
 
960
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
 
961
                          ('4b', '4', 0)],
 
962
                         revisions)
 
963
 
 
964
 
 
965
    def test_file_id_for_range(self):
 
966
        mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
 
967
        wt.lock_read()
 
968
        self.addCleanup(wt.unlock)
 
969
 
 
970
        def rev_from_rev_id(revid, branch):
 
971
            revspec = revisionspec.RevisionSpec.from_string('revid:%s' % revid)
 
972
            return revspec.in_history(branch)
 
973
 
 
974
        def view_revs(start_rev, end_rev, file_id, direction):
 
975
            revs = log.calculate_view_revisions(
 
976
                wt.branch,
 
977
                start_rev, # start_revision
 
978
                end_rev, # end_revision
 
979
                direction, # direction
 
980
                file_id, # specific_fileid
 
981
                True, # generate_merge_revisions
 
982
                True, # allow_single_merge_revision
 
983
                )
 
984
            return revs
 
985
 
 
986
        rev_3a = rev_from_rev_id('3a', wt.branch)
 
987
        rev_4b = rev_from_rev_id('4b', wt.branch)
 
988
        self.assertEqual([('3c', '3', 0), ('3a', '2.1.1', 1)],
 
989
                          view_revs(rev_3a, rev_4b, 'f-id', 'reverse'))
 
990
        # Note that the depth is 0 for 3a because depths are normalized, but
 
991
        # there is still a bug somewhere... most probably in
 
992
        # _filter_revision_range and/or get_view_revisions still around a bad
 
993
        # use of reverse_by_depth
 
994
        self.assertEqual([('3a', '2.1.1', 0)],
 
995
                          view_revs(rev_3a, rev_4b, 'f-id', 'forward'))
 
996
 
 
997
 
 
998
class TestGetRevisionsTouchingFileID(tests.TestCaseWithTransport):
 
999
 
 
1000
    def create_tree_with_single_merge(self):
 
1001
        """Create a branch with a moderate layout.
 
1002
 
 
1003
        The revision graph looks like:
 
1004
 
 
1005
           A
 
1006
           |\
 
1007
           B C
 
1008
           |/
 
1009
           D
 
1010
 
 
1011
        In this graph, A introduced files f1 and f2 and f3.
 
1012
        B modifies f1 and f3, and C modifies f2 and f3.
 
1013
        D merges the changes from B and C and resolves the conflict for f3.
 
1014
        """
 
1015
        # TODO: jam 20070218 This seems like it could really be done
 
1016
        #       with make_branch_and_memory_tree() if we could just
 
1017
        #       create the content of those files.
 
1018
        # TODO: jam 20070218 Another alternative is that we would really
 
1019
        #       like to only create this tree 1 time for all tests that
 
1020
        #       use it. Since 'log' only uses the tree in a readonly
 
1021
        #       fashion, it seems a shame to regenerate an identical
 
1022
        #       tree for each test.
 
1023
        tree = self.make_branch_and_tree('tree')
 
1024
        tree.lock_write()
 
1025
        self.addCleanup(tree.unlock)
 
1026
 
 
1027
        self.build_tree_contents([('tree/f1', 'A\n'),
 
1028
                                  ('tree/f2', 'A\n'),
 
1029
                                  ('tree/f3', 'A\n'),
 
1030
                                 ])
 
1031
        tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
 
1032
        tree.commit('A', rev_id='A')
 
1033
 
 
1034
        self.build_tree_contents([('tree/f2', 'A\nC\n'),
 
1035
                                  ('tree/f3', 'A\nC\n'),
 
1036
                                 ])
 
1037
        tree.commit('C', rev_id='C')
 
1038
        # Revert back to A to build the other history.
 
1039
        tree.set_last_revision('A')
 
1040
        tree.branch.set_last_revision_info(1, 'A')
 
1041
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
 
1042
                                  ('tree/f2', 'A\n'),
 
1043
                                  ('tree/f3', 'A\nB\n'),
 
1044
                                 ])
 
1045
        tree.commit('B', rev_id='B')
 
1046
        tree.set_parent_ids(['B', 'C'])
 
1047
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
 
1048
                                  ('tree/f2', 'A\nC\n'),
 
1049
                                  ('tree/f3', 'A\nB\nC\n'),
 
1050
                                 ])
 
1051
        tree.commit('D', rev_id='D')
 
1052
 
 
1053
        # Switch to a read lock for this tree.
 
1054
        # We still have an addCleanup(tree.unlock) pending
 
1055
        tree.unlock()
 
1056
        tree.lock_read()
 
1057
        return tree
 
1058
 
 
1059
    def check_delta(self, delta, **kw):
 
1060
        """Check the filenames touched by a delta are as expected.
 
1061
 
 
1062
        Caller only have to pass in the list of files for each part, all
 
1063
        unspecified parts are considered empty (and checked as such).
 
1064
        """
 
1065
        for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
 
1066
            # By default we expect an empty list
 
1067
            expected = kw.get(n, [])
 
1068
            # strip out only the path components
 
1069
            got = [x[0] for x in getattr(delta, n)]
 
1070
            self.assertEqual(expected, got)
 
1071
 
 
1072
    def test_tree_with_single_merge(self):
 
1073
        """Make sure the tree layout is correct."""
 
1074
        tree = self.create_tree_with_single_merge()
 
1075
        rev_A_tree = tree.branch.repository.revision_tree('A')
 
1076
        rev_B_tree = tree.branch.repository.revision_tree('B')
 
1077
        rev_C_tree = tree.branch.repository.revision_tree('C')
 
1078
        rev_D_tree = tree.branch.repository.revision_tree('D')
 
1079
 
 
1080
        self.check_delta(rev_B_tree.changes_from(rev_A_tree),
 
1081
                         modified=['f1', 'f3'])
 
1082
 
 
1083
        self.check_delta(rev_C_tree.changes_from(rev_A_tree),
 
1084
                         modified=['f2', 'f3'])
 
1085
 
 
1086
        self.check_delta(rev_D_tree.changes_from(rev_B_tree),
 
1087
                         modified=['f2', 'f3'])
 
1088
 
 
1089
        self.check_delta(rev_D_tree.changes_from(rev_C_tree),
 
1090
                         modified=['f1', 'f3'])
 
1091
 
 
1092
    def assertAllRevisionsForFileID(self, tree, file_id, revisions):
 
1093
        """Ensure _filter_revisions_touching_file_id returns the right values.
 
1094
 
 
1095
        Get the return value from _filter_revisions_touching_file_id and make
 
1096
        sure they are correct.
 
1097
        """
 
1098
        # The api for _filter_revisions_touching_file_id is a little crazy.
 
1099
        # So we do the setup here.
 
1100
        mainline = tree.branch.revision_history()
 
1101
        mainline.insert(0, None)
 
1102
        revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
 
1103
        view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
 
1104
                                                'reverse', True)
 
1105
        actual_revs = log._filter_revisions_touching_file_id(
 
1106
                            tree.branch,
 
1107
                            file_id,
 
1108
                            list(view_revs_iter))
 
1109
        self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
 
1110
 
 
1111
    def test_file_id_f1(self):
 
1112
        tree = self.create_tree_with_single_merge()
 
1113
        # f1 should be marked as modified by revisions A and B
 
1114
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
 
1115
 
 
1116
    def test_file_id_f2(self):
 
1117
        tree = self.create_tree_with_single_merge()
 
1118
        # f2 should be marked as modified by revisions A, C, and D
 
1119
        # because D merged the changes from C.
 
1120
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
 
1121
 
 
1122
    def test_file_id_f3(self):
 
1123
        tree = self.create_tree_with_single_merge()
 
1124
        # f3 should be marked as modified by revisions A, B, C, and D
 
1125
        self.assertAllRevisionsForFileID(tree, 'f3-id', ['D', 'C', 'B', 'A'])
 
1126
 
 
1127
    def test_file_id_with_ghosts(self):
 
1128
        # This is testing bug #209948, where having a ghost would cause
 
1129
        # _filter_revisions_touching_file_id() to fail.
 
1130
        tree = self.create_tree_with_single_merge()
 
1131
        # We need to add a revision, so switch back to a write-locked tree
 
1132
        # (still a single addCleanup(tree.unlock) pending).
 
1133
        tree.unlock()
 
1134
        tree.lock_write()
 
1135
        first_parent = tree.last_revision()
 
1136
        tree.set_parent_ids([first_parent, 'ghost-revision-id'])
 
1137
        self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
 
1138
        tree.commit('commit with a ghost', rev_id='XX')
 
1139
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
 
1140
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
 
1141
 
 
1142
 
 
1143
class TestShowChangedRevisions(tests.TestCaseWithTransport):
 
1144
 
 
1145
    def test_show_changed_revisions_verbose(self):
 
1146
        tree = self.make_branch_and_tree('tree_a')
 
1147
        self.build_tree(['tree_a/foo'])
 
1148
        tree.add('foo')
 
1149
        tree.commit('bar', rev_id='bar-id')
 
1150
        s = self.make_utf8_encoded_stringio()
 
1151
        log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
 
1152
        self.assertContainsRe(s.getvalue(), 'bar')
 
1153
        self.assertNotContainsRe(s.getvalue(), 'foo')
 
1154
 
 
1155
 
 
1156
class TestLogFormatter(tests.TestCase):
 
1157
 
 
1158
    def test_short_committer(self):
 
1159
        rev = revision.Revision('a-id')
 
1160
        rev.committer = 'John Doe <jdoe@example.com>'
 
1161
        lf = log.LogFormatter(None)
 
1162
        self.assertEqual('John Doe', lf.short_committer(rev))
 
1163
        rev.committer = 'John Smith <jsmith@example.com>'
 
1164
        self.assertEqual('John Smith', lf.short_committer(rev))
 
1165
        rev.committer = 'John Smith'
 
1166
        self.assertEqual('John Smith', lf.short_committer(rev))
 
1167
        rev.committer = 'jsmith@example.com'
 
1168
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
 
1169
        rev.committer = '<jsmith@example.com>'
 
1170
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
 
1171
        rev.committer = 'John Smith jsmith@example.com'
 
1172
        self.assertEqual('John Smith', lf.short_committer(rev))
 
1173
 
 
1174
    def test_short_author(self):
 
1175
        rev = revision.Revision('a-id')
 
1176
        rev.committer = 'John Doe <jdoe@example.com>'
 
1177
        lf = log.LogFormatter(None)
 
1178
        self.assertEqual('John Doe', lf.short_author(rev))
 
1179
        rev.properties['author'] = 'John Smith <jsmith@example.com>'
 
1180
        self.assertEqual('John Smith', lf.short_author(rev))
 
1181
        rev.properties['author'] = 'John Smith'
 
1182
        self.assertEqual('John Smith', lf.short_author(rev))
 
1183
        rev.properties['author'] = 'jsmith@example.com'
 
1184
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
 
1185
        rev.properties['author'] = '<jsmith@example.com>'
 
1186
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
 
1187
        rev.properties['author'] = 'John Smith jsmith@example.com'
 
1188
        self.assertEqual('John Smith', lf.short_author(rev))
 
1189
 
 
1190
 
 
1191
class TestReverseByDepth(tests.TestCase):
 
1192
    """Test reverse_by_depth behavior.
 
1193
 
 
1194
    This is used to present revisions in forward (oldest first) order in a nice
 
1195
    layout.
 
1196
 
 
1197
    The tests use lighter revision description to ease reading.
 
1198
    """
 
1199
 
 
1200
    def assertReversed(self, forward, backward):
 
1201
        # Transform the descriptions to suit the API: tests use (revno, depth),
 
1202
        # while the API expects (revid, revno, depth)
 
1203
        def complete_revisions(l):
 
1204
            """Transform the description to suit the API.
 
1205
 
 
1206
            Tests use (revno, depth) whil the API expects (revid, revno, depth).
 
1207
            Since the revid is arbitrary, we just duplicate revno
 
1208
            """
 
1209
            return [ (r, r, d) for r, d in l]
 
1210
        forward = complete_revisions(forward)
 
1211
        backward= complete_revisions(backward)
 
1212
        self.assertEqual(forward, log.reverse_by_depth(backward))
 
1213
 
 
1214
 
 
1215
    def test_mainline_revisions(self):
 
1216
        self.assertReversed([( '1', 0), ('2', 0)],
 
1217
                            [('2', 0), ('1', 0)])
 
1218
 
 
1219
    def test_merged_revisions(self):
 
1220
        self.assertReversed([('1', 0), ('2', 0), ('2.2', 1), ('2.1', 1),],
 
1221
                            [('2', 0), ('2.1', 1), ('2.2', 1), ('1', 0),])
 
1222
    def test_shifted_merged_revisions(self):
 
1223
        """Test irregular layout.
 
1224
 
 
1225
        Requesting revisions touching a file can produce "holes" in the depths.
 
1226
        """
 
1227
        self.assertReversed([('1', 0), ('2', 0), ('1.1', 2), ('1.2', 2),],
 
1228
                            [('2', 0), ('1.2', 2), ('1.1', 2), ('1', 0),])
 
1229
 
 
1230
    def test_merged_without_child_revisions(self):
 
1231
        """Test irregular layout.
 
1232
 
 
1233
        Revision ranges can produce "holes" in the depths.
 
1234
        """
 
1235
        # When a revision of higher depth doesn't follow one of lower depth, we
 
1236
        # assume a lower depth one is virtually there
 
1237
        self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
 
1238
                            [('4', 4), ('3', 3), ('2', 2), ('1', 2),])
 
1239
        # So we get the same order after reversing below even if the original
 
1240
        # revisions are not in the same order.
 
1241
        self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
 
1242
                            [('3', 3), ('4', 4), ('2', 2), ('1', 2),])
 
1243
 
 
1244
 
 
1245
class TestHistoryChange(tests.TestCaseWithTransport):
 
1246
 
 
1247
    def setup_a_tree(self):
 
1248
        tree = self.make_branch_and_tree('tree')
 
1249
        tree.lock_write()
 
1250
        self.addCleanup(tree.unlock)
 
1251
        tree.commit('1a', rev_id='1a')
 
1252
        tree.commit('2a', rev_id='2a')
 
1253
        tree.commit('3a', rev_id='3a')
 
1254
        return tree
 
1255
 
 
1256
    def setup_ab_tree(self):
 
1257
        tree = self.setup_a_tree()
 
1258
        tree.set_last_revision('1a')
 
1259
        tree.branch.set_last_revision_info(1, '1a')
 
1260
        tree.commit('2b', rev_id='2b')
 
1261
        tree.commit('3b', rev_id='3b')
 
1262
        return tree
 
1263
 
 
1264
    def setup_ac_tree(self):
 
1265
        tree = self.setup_a_tree()
 
1266
        tree.set_last_revision(revision.NULL_REVISION)
 
1267
        tree.branch.set_last_revision_info(0, revision.NULL_REVISION)
 
1268
        tree.commit('1c', rev_id='1c')
 
1269
        tree.commit('2c', rev_id='2c')
 
1270
        tree.commit('3c', rev_id='3c')
 
1271
        return tree
 
1272
 
 
1273
    def test_all_new(self):
 
1274
        tree = self.setup_ab_tree()
 
1275
        old, new = log.get_history_change('1a', '3a', tree.branch.repository)
 
1276
        self.assertEqual([], old)
 
1277
        self.assertEqual(['2a', '3a'], new)
 
1278
 
 
1279
    def test_all_old(self):
 
1280
        tree = self.setup_ab_tree()
 
1281
        old, new = log.get_history_change('3a', '1a', tree.branch.repository)
 
1282
        self.assertEqual([], new)
 
1283
        self.assertEqual(['2a', '3a'], old)
 
1284
 
 
1285
    def test_null_old(self):
 
1286
        tree = self.setup_ab_tree()
 
1287
        old, new = log.get_history_change(revision.NULL_REVISION,
 
1288
                                          '3a', tree.branch.repository)
 
1289
        self.assertEqual([], old)
 
1290
        self.assertEqual(['1a', '2a', '3a'], new)
 
1291
 
 
1292
    def test_null_new(self):
 
1293
        tree = self.setup_ab_tree()
 
1294
        old, new = log.get_history_change('3a', revision.NULL_REVISION,
 
1295
                                          tree.branch.repository)
 
1296
        self.assertEqual([], new)
 
1297
        self.assertEqual(['1a', '2a', '3a'], old)
 
1298
 
 
1299
    def test_diverged(self):
 
1300
        tree = self.setup_ab_tree()
 
1301
        old, new = log.get_history_change('3a', '3b', tree.branch.repository)
 
1302
        self.assertEqual(old, ['2a', '3a'])
 
1303
        self.assertEqual(new, ['2b', '3b'])
 
1304
 
 
1305
    def test_unrelated(self):
 
1306
        tree = self.setup_ac_tree()
 
1307
        old, new = log.get_history_change('3a', '3c', tree.branch.repository)
 
1308
        self.assertEqual(old, ['1a', '2a', '3a'])
 
1309
        self.assertEqual(new, ['1c', '2c', '3c'])
 
1310
 
 
1311
    def test_show_branch_change(self):
 
1312
        tree = self.setup_ab_tree()
 
1313
        s = StringIO()
 
1314
        log.show_branch_change(tree.branch, s, 3, '3a')
 
1315
        self.assertContainsRe(s.getvalue(),
 
1316
            '[*]{60}\nRemoved Revisions:\n(.|\n)*2a(.|\n)*3a(.|\n)*'
 
1317
            '[*]{60}\n\nAdded Revisions:\n(.|\n)*2b(.|\n)*3b')
 
1318
 
 
1319
    def test_show_branch_change_no_change(self):
 
1320
        tree = self.setup_ab_tree()
 
1321
        s = StringIO()
 
1322
        log.show_branch_change(tree.branch, s, 3, '3b')
 
1323
        self.assertEqual(s.getvalue(),
 
1324
            'Nothing seems to have changed\n')
 
1325
 
 
1326
    def test_show_branch_change_no_old(self):
 
1327
        tree = self.setup_ab_tree()
 
1328
        s = StringIO()
 
1329
        log.show_branch_change(tree.branch, s, 2, '2b')
 
1330
        self.assertContainsRe(s.getvalue(), 'Added Revisions:')
 
1331
        self.assertNotContainsRe(s.getvalue(), 'Removed Revisions:')
 
1332
 
 
1333
    def test_show_branch_change_no_new(self):
 
1334
        tree = self.setup_ab_tree()
 
1335
        tree.branch.set_last_revision_info(2, '2b')
 
1336
        s = StringIO()
 
1337
        log.show_branch_change(tree.branch, s, 3, '3b')
 
1338
        self.assertContainsRe(s.getvalue(), 'Removed Revisions:')
 
1339
        self.assertNotContainsRe(s.getvalue(), 'Added Revisions:')