~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_log.py

  • Committer: Martin Pool
  • Author(s): Jari Aalto
  • Date: 2008-12-24 03:14:16 UTC
  • mto: This revision was merged to the branch mainline in revision 3919.
  • Revision ID: mbp@sourcefrog.net-20081224031416-krocx1r3fyu52t0j
In user guide, use 'PROJECT' as a metavariable not 'X-repo'

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 test_short_log_with_merges(self):
 
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
        logfile = self.make_utf8_encoded_stringio()
 
310
        formatter = log.ShortLogFormatter(to_file=logfile)
 
311
        log.show_log(wt.branch, formatter)
 
312
        self.assertEqualDiff("""\
 
313
    2 Joe Foo\t2005-11-22 [merge]
 
314
      rev-2
 
315
 
 
316
    1 Joe Foo\t2005-11-22
 
317
      rev-1
 
318
 
 
319
""",
 
320
                             logfile.getvalue())
 
321
 
 
322
    def test_short_log_single_merge_revision(self):
 
323
        wt = self.make_branch_and_memory_tree('.')
 
324
        wt.lock_write()
 
325
        self.addCleanup(wt.unlock)
 
326
        wt.add('')
 
327
        wt.commit('rev-1', rev_id='rev-1',
 
328
                  timestamp=1132586655, timezone=36000,
 
329
                  committer='Joe Foo <joe@foo.com>')
 
330
        wt.commit('rev-merged', rev_id='rev-2a',
 
331
                  timestamp=1132586700, timezone=36000,
 
332
                  committer='Joe Foo <joe@foo.com>')
 
333
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
334
        wt.branch.set_last_revision_info(1, 'rev-1')
 
335
        wt.commit('rev-2', rev_id='rev-2b',
 
336
                  timestamp=1132586800, timezone=36000,
 
337
                  committer='Joe Foo <joe@foo.com>')
 
338
        logfile = self.make_utf8_encoded_stringio()
 
339
        formatter = log.ShortLogFormatter(to_file=logfile)
 
340
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
 
341
        wtb = wt.branch
 
342
        rev = revspec.in_history(wtb)
 
343
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
344
        self.assertEqualDiff("""\
 
345
1.1.1 Joe Foo\t2005-11-22
 
346
      rev-merged
 
347
 
 
348
""",
 
349
                             logfile.getvalue())
 
350
 
 
351
 
 
352
class TestLongLogFormatter(TestCaseWithoutPropsHandler):
 
353
 
 
354
    def test_verbose_log(self):
 
355
        """Verbose log includes changed files
 
356
        
 
357
        bug #4676
 
358
        """
 
359
        wt = self.make_branch_and_tree('.')
 
360
        b = wt.branch
 
361
        self.build_tree(['a'])
 
362
        wt.add('a')
 
363
        # XXX: why does a longer nick show up?
 
364
        b.nick = 'test_verbose_log'
 
365
        wt.commit(message='add a',
 
366
                  timestamp=1132711707,
 
367
                  timezone=36000,
 
368
                  committer='Lorem Ipsum <test@example.com>')
 
369
        logfile = file('out.tmp', 'w+')
 
370
        formatter = log.LongLogFormatter(to_file=logfile)
 
371
        log.show_log(b, formatter, verbose=True)
 
372
        logfile.flush()
 
373
        logfile.seek(0)
 
374
        log_contents = logfile.read()
 
375
        self.assertEqualDiff('''\
 
376
------------------------------------------------------------
 
377
revno: 1
 
378
committer: Lorem Ipsum <test@example.com>
 
379
branch nick: test_verbose_log
 
380
timestamp: Wed 2005-11-23 12:08:27 +1000
 
381
message:
 
382
  add a
 
383
added:
 
384
  a
 
385
''',
 
386
                             log_contents)
 
387
 
 
388
    def test_merges_are_indented_by_level(self):
 
389
        wt = self.make_branch_and_tree('parent')
 
390
        wt.commit('first post')
 
391
        self.run_bzr('branch parent child')
 
392
        self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
 
393
        self.run_bzr('branch child smallerchild')
 
394
        self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
 
395
            'smallerchild'])
 
396
        os.chdir('child')
 
397
        self.run_bzr('merge ../smallerchild')
 
398
        self.run_bzr(['commit', '-m', 'merge branch 2'])
 
399
        os.chdir('../parent')
 
400
        self.run_bzr('merge ../child')
 
401
        wt.commit('merge branch 1')
 
402
        b = wt.branch
 
403
        sio = self.make_utf8_encoded_stringio()
 
404
        lf = log.LongLogFormatter(to_file=sio)
 
405
        log.show_log(b, lf, verbose=True)
 
406
        the_log = normalize_log(sio.getvalue())
 
407
        self.assertEqualDiff("""\
 
408
------------------------------------------------------------
 
409
revno: 2
 
410
committer: Lorem Ipsum <test@example.com>
 
411
branch nick: parent
 
412
timestamp: Just now
 
413
message:
 
414
  merge branch 1
 
415
    ------------------------------------------------------------
 
416
    revno: 1.1.2
 
417
    committer: Lorem Ipsum <test@example.com>
 
418
    branch nick: child
 
419
    timestamp: Just now
 
420
    message:
 
421
      merge branch 2
 
422
        ------------------------------------------------------------
 
423
        revno: 1.2.1
 
424
        committer: Lorem Ipsum <test@example.com>
 
425
        branch nick: smallerchild
 
426
        timestamp: Just now
 
427
        message:
 
428
          branch 2
 
429
    ------------------------------------------------------------
 
430
    revno: 1.1.1
 
431
    committer: Lorem Ipsum <test@example.com>
 
432
    branch nick: child
 
433
    timestamp: Just now
 
434
    message:
 
435
      branch 1
 
436
------------------------------------------------------------
 
437
revno: 1
 
438
committer: Lorem Ipsum <test@example.com>
 
439
branch nick: parent
 
440
timestamp: Just now
 
441
message:
 
442
  first post
 
443
""",
 
444
                             the_log)
 
445
 
 
446
    def test_verbose_merge_revisions_contain_deltas(self):
 
447
        wt = self.make_branch_and_tree('parent')
 
448
        self.build_tree(['parent/f1', 'parent/f2'])
 
449
        wt.add(['f1','f2'])
 
450
        wt.commit('first post')
 
451
        self.run_bzr('branch parent child')
 
452
        os.unlink('child/f1')
 
453
        file('child/f2', 'wb').write('hello\n')
 
454
        self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
 
455
            'child'])
 
456
        os.chdir('parent')
 
457
        self.run_bzr('merge ../child')
 
458
        wt.commit('merge branch 1')
 
459
        b = wt.branch
 
460
        sio = self.make_utf8_encoded_stringio()
 
461
        lf = log.LongLogFormatter(to_file=sio)
 
462
        log.show_log(b, lf, verbose=True)
 
463
        the_log = normalize_log(sio.getvalue())
 
464
        self.assertEqualDiff("""\
 
465
------------------------------------------------------------
 
466
revno: 2
 
467
committer: Lorem Ipsum <test@example.com>
 
468
branch nick: parent
 
469
timestamp: Just now
 
470
message:
 
471
  merge branch 1
 
472
removed:
 
473
  f1
 
474
modified:
 
475
  f2
 
476
    ------------------------------------------------------------
 
477
    revno: 1.1.1
 
478
    committer: Lorem Ipsum <test@example.com>
 
479
    branch nick: child
 
480
    timestamp: Just now
 
481
    message:
 
482
      removed f1 and modified f2
 
483
    removed:
 
484
      f1
 
485
    modified:
 
486
      f2
 
487
------------------------------------------------------------
 
488
revno: 1
 
489
committer: Lorem Ipsum <test@example.com>
 
490
branch nick: parent
 
491
timestamp: Just now
 
492
message:
 
493
  first post
 
494
added:
 
495
  f1
 
496
  f2
 
497
""",
 
498
                             the_log)
 
499
 
 
500
    def test_trailing_newlines(self):
 
501
        wt = self.make_branch_and_tree('.')
 
502
        b = make_commits_with_trailing_newlines(wt)
 
503
        sio = self.make_utf8_encoded_stringio()
 
504
        lf = log.LongLogFormatter(to_file=sio)
 
505
        log.show_log(b, lf)
 
506
        self.assertEqualDiff("""\
 
507
------------------------------------------------------------
 
508
revno: 3
 
509
committer: Joe Foo <joe@foo.com>
 
510
branch nick: test
 
511
timestamp: Mon 2005-11-21 09:32:56 -0600
 
512
message:
 
513
  single line with trailing newline
 
514
------------------------------------------------------------
 
515
revno: 2
 
516
author: Joe Bar <joe@bar.com>
 
517
committer: Joe Foo <joe@foo.com>
 
518
branch nick: test
 
519
timestamp: Mon 2005-11-21 09:27:22 -0600
 
520
message:
 
521
  multiline
 
522
  log
 
523
  message
 
524
------------------------------------------------------------
 
525
revno: 1
 
526
committer: Joe Foo <joe@foo.com>
 
527
branch nick: test
 
528
timestamp: Mon 2005-11-21 09:24:15 -0600
 
529
message:
 
530
  simple log message
 
531
""",
 
532
                             sio.getvalue())
 
533
 
 
534
    def test_author_in_log(self):
 
535
        """Log includes the author name if it's set in
 
536
        the revision properties
 
537
        """
 
538
        wt = self.make_branch_and_tree('.')
 
539
        b = wt.branch
 
540
        self.build_tree(['a'])
 
541
        wt.add('a')
 
542
        b.nick = 'test_author_log'
 
543
        wt.commit(message='add a',
 
544
                  timestamp=1132711707,
 
545
                  timezone=36000,
 
546
                  committer='Lorem Ipsum <test@example.com>',
 
547
                  author='John Doe <jdoe@example.com>')
 
548
        sio = StringIO()
 
549
        formatter = log.LongLogFormatter(to_file=sio)
 
550
        log.show_log(b, formatter)
 
551
        self.assertEqualDiff('''\
 
552
------------------------------------------------------------
 
553
revno: 1
 
554
author: John Doe <jdoe@example.com>
 
555
committer: Lorem Ipsum <test@example.com>
 
556
branch nick: test_author_log
 
557
timestamp: Wed 2005-11-23 12:08:27 +1000
 
558
message:
 
559
  add a
 
560
''',
 
561
                             sio.getvalue())
 
562
 
 
563
    def test_properties_in_log(self):
 
564
        """Log includes the custom properties returned by the registered 
 
565
        handlers.
 
566
        """
 
567
        wt = self.make_branch_and_tree('.')
 
568
        b = wt.branch
 
569
        self.build_tree(['a'])
 
570
        wt.add('a')
 
571
        b.nick = 'test_properties_in_log'
 
572
        wt.commit(message='add a',
 
573
                  timestamp=1132711707,
 
574
                  timezone=36000,
 
575
                  committer='Lorem Ipsum <test@example.com>',
 
576
                  author='John Doe <jdoe@example.com>')
 
577
        sio = StringIO()
 
578
        formatter = log.LongLogFormatter(to_file=sio)
 
579
        try:
 
580
            def trivial_custom_prop_handler(revision):
 
581
                return {'test_prop':'test_value'}
 
582
 
 
583
            log.properties_handler_registry.register(
 
584
                'trivial_custom_prop_handler',
 
585
                trivial_custom_prop_handler)
 
586
            log.show_log(b, formatter)
 
587
        finally:
 
588
            log.properties_handler_registry.remove(
 
589
                'trivial_custom_prop_handler')
 
590
            self.assertEqualDiff('''\
 
591
------------------------------------------------------------
 
592
revno: 1
 
593
test_prop: test_value
 
594
author: John Doe <jdoe@example.com>
 
595
committer: Lorem Ipsum <test@example.com>
 
596
branch nick: test_properties_in_log
 
597
timestamp: Wed 2005-11-23 12:08:27 +1000
 
598
message:
 
599
  add a
 
600
''',
 
601
                                 sio.getvalue())
 
602
 
 
603
    def test_error_in_properties_handler(self):
 
604
        """Log includes the custom properties returned by the registered 
 
605
        handlers.
 
606
        """
 
607
        wt = self.make_branch_and_tree('.')
 
608
        b = wt.branch
 
609
        self.build_tree(['a'])
 
610
        wt.add('a')
 
611
        b.nick = 'test_author_log'
 
612
        wt.commit(message='add a',
 
613
                  timestamp=1132711707,
 
614
                  timezone=36000,
 
615
                  committer='Lorem Ipsum <test@example.com>',
 
616
                  author='John Doe <jdoe@example.com>',
 
617
                  revprops={'first_prop':'first_value'})
 
618
        sio = StringIO()
 
619
        formatter = log.LongLogFormatter(to_file=sio)
 
620
        try:
 
621
            def trivial_custom_prop_handler(revision):
 
622
                raise StandardError("a test error")
 
623
 
 
624
            log.properties_handler_registry.register(
 
625
                'trivial_custom_prop_handler',
 
626
                trivial_custom_prop_handler)
 
627
            self.assertRaises(StandardError, log.show_log, b, formatter,)
 
628
        finally:
 
629
            log.properties_handler_registry.remove(
 
630
                'trivial_custom_prop_handler')
 
631
 
 
632
    def test_properties_handler_bad_argument(self):
 
633
        wt = self.make_branch_and_tree('.')
 
634
        b = wt.branch
 
635
        self.build_tree(['a'])
 
636
        wt.add('a')
 
637
        b.nick = 'test_author_log'
 
638
        wt.commit(message='add a',
 
639
                  timestamp=1132711707,
 
640
                  timezone=36000,
 
641
                  committer='Lorem Ipsum <test@example.com>',
 
642
                  author='John Doe <jdoe@example.com>',
 
643
                  revprops={'a_prop':'test_value'})
 
644
        sio = StringIO()
 
645
        formatter = log.LongLogFormatter(to_file=sio)
 
646
        try:
 
647
            def bad_argument_prop_handler(revision):
 
648
                return {'custom_prop_name':revision.properties['a_prop']}
 
649
 
 
650
            log.properties_handler_registry.register(
 
651
                'bad_argument_prop_handler',
 
652
                bad_argument_prop_handler)
 
653
 
 
654
            self.assertRaises(AttributeError, formatter.show_properties,
 
655
                              'a revision', '')
 
656
 
 
657
            revision = b.repository.get_revision(b.last_revision())
 
658
            formatter.show_properties(revision, '')
 
659
            self.assertEqualDiff('''custom_prop_name: test_value\n''',
 
660
                                 sio.getvalue())
 
661
        finally:
 
662
            log.properties_handler_registry.remove(
 
663
                'bad_argument_prop_handler')
 
664
 
 
665
 
 
666
class TestLineLogFormatter(tests.TestCaseWithTransport):
 
667
 
 
668
    def test_line_log(self):
 
669
        """Line log should show revno
 
670
        
 
671
        bug #5162
 
672
        """
 
673
        wt = self.make_branch_and_tree('.')
 
674
        b = wt.branch
 
675
        self.build_tree(['a'])
 
676
        wt.add('a')
 
677
        b.nick = 'test-line-log'
 
678
        wt.commit(message='add a',
 
679
                  timestamp=1132711707,
 
680
                  timezone=36000,
 
681
                  committer='Line-Log-Formatter Tester <test@line.log>')
 
682
        logfile = file('out.tmp', 'w+')
 
683
        formatter = log.LineLogFormatter(to_file=logfile)
 
684
        log.show_log(b, formatter)
 
685
        logfile.flush()
 
686
        logfile.seek(0)
 
687
        log_contents = logfile.read()
 
688
        self.assertEqualDiff('1: Line-Log-Formatte... 2005-11-23 add a\n',
 
689
                             log_contents)
 
690
 
 
691
    def test_trailing_newlines(self):
 
692
        wt = self.make_branch_and_tree('.')
 
693
        b = make_commits_with_trailing_newlines(wt)
 
694
        sio = self.make_utf8_encoded_stringio()
 
695
        lf = log.LineLogFormatter(to_file=sio)
 
696
        log.show_log(b, lf)
 
697
        self.assertEqualDiff("""\
 
698
3: Joe Foo 2005-11-21 single line with trailing newline
 
699
2: Joe Bar 2005-11-21 multiline
 
700
1: Joe Foo 2005-11-21 simple log message
 
701
""",
 
702
                             sio.getvalue())
 
703
 
 
704
    def test_line_log_single_merge_revision(self):
 
705
        wt = self.make_branch_and_memory_tree('.')
 
706
        wt.lock_write()
 
707
        self.addCleanup(wt.unlock)
 
708
        wt.add('')
 
709
        wt.commit('rev-1', rev_id='rev-1',
 
710
                  timestamp=1132586655, timezone=36000,
 
711
                  committer='Joe Foo <joe@foo.com>')
 
712
        wt.commit('rev-merged', rev_id='rev-2a',
 
713
                  timestamp=1132586700, timezone=36000,
 
714
                  committer='Joe Foo <joe@foo.com>')
 
715
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
716
        wt.branch.set_last_revision_info(1, 'rev-1')
 
717
        wt.commit('rev-2', rev_id='rev-2b',
 
718
                  timestamp=1132586800, timezone=36000,
 
719
                  committer='Joe Foo <joe@foo.com>')
 
720
        logfile = self.make_utf8_encoded_stringio()
 
721
        formatter = log.LineLogFormatter(to_file=logfile)
 
722
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
 
723
        wtb = wt.branch
 
724
        rev = revspec.in_history(wtb)
 
725
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
726
        self.assertEqualDiff("""\
 
727
1.1.1: Joe Foo 2005-11-22 rev-merged
 
728
""",
 
729
                             logfile.getvalue())
 
730
 
 
731
 
 
732
 
 
733
class TestGetViewRevisions(tests.TestCaseWithTransport):
 
734
 
 
735
    def make_tree_with_commits(self):
 
736
        """Create a tree with well-known revision ids"""
 
737
        wt = self.make_branch_and_tree('tree1')
 
738
        wt.commit('commit one', rev_id='1')
 
739
        wt.commit('commit two', rev_id='2')
 
740
        wt.commit('commit three', rev_id='3')
 
741
        mainline_revs = [None, '1', '2', '3']
 
742
        rev_nos = {'1': 1, '2': 2, '3': 3}
 
743
        return mainline_revs, rev_nos, wt
 
744
 
 
745
    def make_tree_with_merges(self):
 
746
        """Create a tree with well-known revision ids and a merge"""
 
747
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
 
748
        tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
 
749
        tree2.commit('four-a', rev_id='4a')
 
750
        wt.merge_from_branch(tree2.branch)
 
751
        wt.commit('four-b', rev_id='4b')
 
752
        mainline_revs.append('4b')
 
753
        rev_nos['4b'] = 4
 
754
        # 4a: 3.1.1
 
755
        return mainline_revs, rev_nos, wt
 
756
 
 
757
    def make_tree_with_many_merges(self):
 
758
        """Create a tree with well-known revision ids"""
 
759
        wt = self.make_branch_and_tree('tree1')
 
760
        self.build_tree_contents([('tree1/f', '1\n')])
 
761
        wt.add(['f'], ['f-id'])
 
762
        wt.commit('commit one', rev_id='1')
 
763
        wt.commit('commit two', rev_id='2')
 
764
 
 
765
        tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
 
766
        self.build_tree_contents([('tree3/f', '1\n2\n3a\n')])
 
767
        tree3.commit('commit three a', rev_id='3a')
 
768
 
 
769
        tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
 
770
        tree2.merge_from_branch(tree3.branch)
 
771
        tree2.commit('commit three b', rev_id='3b')
 
772
 
 
773
        wt.merge_from_branch(tree2.branch)
 
774
        wt.commit('commit three c', rev_id='3c')
 
775
        tree2.commit('four-a', rev_id='4a')
 
776
 
 
777
        wt.merge_from_branch(tree2.branch)
 
778
        wt.commit('four-b', rev_id='4b')
 
779
 
 
780
        mainline_revs = [None, '1', '2', '3c', '4b']
 
781
        rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
 
782
        full_rev_nos_for_reference = {
 
783
            '1': '1',
 
784
            '2': '2',
 
785
            '3a': '2.1.1', #first commit tree 3
 
786
            '3b': '2.2.1', # first commit tree 2
 
787
            '3c': '3', #merges 3b to main
 
788
            '4a': '2.2.2', # second commit tree 2
 
789
            '4b': '4', # merges 4a to main
 
790
            }
 
791
        return mainline_revs, rev_nos, wt
 
792
 
 
793
    def test_get_view_revisions_forward(self):
 
794
        """Test the get_view_revisions method"""
 
795
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
 
796
        wt.lock_read()
 
797
        self.addCleanup(wt.unlock)
 
798
        revisions = list(log.get_view_revisions(
 
799
                mainline_revs, rev_nos, wt.branch, 'forward'))
 
800
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
 
801
                         revisions)
 
802
        revisions2 = list(log.get_view_revisions(
 
803
                mainline_revs, rev_nos, wt.branch, 'forward',
 
804
                include_merges=False))
 
805
        self.assertEqual(revisions, revisions2)
 
806
 
 
807
    def test_get_view_revisions_reverse(self):
 
808
        """Test the get_view_revisions with reverse"""
 
809
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
 
810
        wt.lock_read()
 
811
        self.addCleanup(wt.unlock)
 
812
        revisions = list(log.get_view_revisions(
 
813
                mainline_revs, rev_nos, wt.branch, 'reverse'))
 
814
        self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
 
815
                         revisions)
 
816
        revisions2 = list(log.get_view_revisions(
 
817
                mainline_revs, rev_nos, wt.branch, 'reverse',
 
818
                include_merges=False))
 
819
        self.assertEqual(revisions, revisions2)
 
820
 
 
821
    def test_get_view_revisions_merge(self):
 
822
        """Test get_view_revisions when there are merges"""
 
823
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
 
824
        wt.lock_read()
 
825
        self.addCleanup(wt.unlock)
 
826
        revisions = list(log.get_view_revisions(
 
827
                mainline_revs, rev_nos, wt.branch, 'forward'))
 
828
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
 
829
                          ('4b', '4', 0), ('4a', '3.1.1', 1)],
 
830
                         revisions)
 
831
        revisions = list(log.get_view_revisions(
 
832
                mainline_revs, rev_nos, wt.branch, 'forward',
 
833
                include_merges=False))
 
834
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
 
835
                          ('4b', '4', 0)],
 
836
                         revisions)
 
837
 
 
838
    def test_get_view_revisions_merge_reverse(self):
 
839
        """Test get_view_revisions in reverse when there are merges"""
 
840
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
 
841
        wt.lock_read()
 
842
        self.addCleanup(wt.unlock)
 
843
        revisions = list(log.get_view_revisions(
 
844
                mainline_revs, rev_nos, wt.branch, 'reverse'))
 
845
        self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
 
846
                          ('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
 
847
                         revisions)
 
848
        revisions = list(log.get_view_revisions(
 
849
                mainline_revs, rev_nos, wt.branch, 'reverse',
 
850
                include_merges=False))
 
851
        self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
 
852
                          ('1', '1', 0)],
 
853
                         revisions)
 
854
 
 
855
    def test_get_view_revisions_merge2(self):
 
856
        """Test get_view_revisions when there are merges"""
 
857
        mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
 
858
        wt.lock_read()
 
859
        self.addCleanup(wt.unlock)
 
860
        revisions = list(log.get_view_revisions(
 
861
                mainline_revs, rev_nos, wt.branch, 'forward'))
 
862
        expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
 
863
                    ('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
 
864
                    ('4a', '2.2.2', 1)]
 
865
        self.assertEqual(expected, revisions)
 
866
        revisions = list(log.get_view_revisions(
 
867
                mainline_revs, rev_nos, wt.branch, 'forward',
 
868
                include_merges=False))
 
869
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
 
870
                          ('4b', '4', 0)],
 
871
                         revisions)
 
872
 
 
873
 
 
874
    def test_file_id_for_range(self):
 
875
        mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
 
876
        wt.lock_read()
 
877
        self.addCleanup(wt.unlock)
 
878
 
 
879
        def rev_from_rev_id(revid, branch):
 
880
            revspec = revisionspec.RevisionSpec.from_string('revid:%s' % revid)
 
881
            return revspec.in_history(branch)
 
882
 
 
883
        def view_revs(start_rev, end_rev, file_id, direction):
 
884
            revs = log.calculate_view_revisions(
 
885
                wt.branch,
 
886
                start_rev, # start_revision
 
887
                end_rev, # end_revision
 
888
                direction, # direction
 
889
                file_id, # specific_fileid
 
890
                True, # generate_merge_revisions
 
891
                True, # allow_single_merge_revision
 
892
                )
 
893
            return revs
 
894
 
 
895
        rev_3a = rev_from_rev_id('3a', wt.branch)
 
896
        rev_4b = rev_from_rev_id('4b', wt.branch)
 
897
        self.assertEqual([('3c', '3', 0), ('3a', '2.1.1', 1)],
 
898
                          view_revs(rev_3a, rev_4b, 'f-id', 'reverse'))
 
899
        # Note that the depth is 0 for 3a because depths are normalized, but
 
900
        # there is still a bug somewhere... most probably in
 
901
        # _filter_revision_range and/or get_view_revisions still around a bad
 
902
        # use of reverse_by_depth
 
903
        self.assertEqual([('3a', '2.1.1', 0)],
 
904
                          view_revs(rev_3a, rev_4b, 'f-id', 'forward'))
 
905
 
 
906
 
 
907
class TestGetRevisionsTouchingFileID(tests.TestCaseWithTransport):
 
908
 
 
909
    def create_tree_with_single_merge(self):
 
910
        """Create a branch with a moderate layout.
 
911
 
 
912
        The revision graph looks like:
 
913
 
 
914
           A
 
915
           |\
 
916
           B C
 
917
           |/
 
918
           D
 
919
 
 
920
        In this graph, A introduced files f1 and f2 and f3.
 
921
        B modifies f1 and f3, and C modifies f2 and f3.
 
922
        D merges the changes from B and C and resolves the conflict for f3.
 
923
        """
 
924
        # TODO: jam 20070218 This seems like it could really be done
 
925
        #       with make_branch_and_memory_tree() if we could just
 
926
        #       create the content of those files.
 
927
        # TODO: jam 20070218 Another alternative is that we would really
 
928
        #       like to only create this tree 1 time for all tests that
 
929
        #       use it. Since 'log' only uses the tree in a readonly
 
930
        #       fashion, it seems a shame to regenerate an identical
 
931
        #       tree for each test.
 
932
        tree = self.make_branch_and_tree('tree')
 
933
        tree.lock_write()
 
934
        self.addCleanup(tree.unlock)
 
935
 
 
936
        self.build_tree_contents([('tree/f1', 'A\n'),
 
937
                                  ('tree/f2', 'A\n'),
 
938
                                  ('tree/f3', 'A\n'),
 
939
                                 ])
 
940
        tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
 
941
        tree.commit('A', rev_id='A')
 
942
 
 
943
        self.build_tree_contents([('tree/f2', 'A\nC\n'),
 
944
                                  ('tree/f3', 'A\nC\n'),
 
945
                                 ])
 
946
        tree.commit('C', rev_id='C')
 
947
        # Revert back to A to build the other history.
 
948
        tree.set_last_revision('A')
 
949
        tree.branch.set_last_revision_info(1, 'A')
 
950
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
 
951
                                  ('tree/f2', 'A\n'),
 
952
                                  ('tree/f3', 'A\nB\n'),
 
953
                                 ])
 
954
        tree.commit('B', rev_id='B')
 
955
        tree.set_parent_ids(['B', 'C'])
 
956
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
 
957
                                  ('tree/f2', 'A\nC\n'),
 
958
                                  ('tree/f3', 'A\nB\nC\n'),
 
959
                                 ])
 
960
        tree.commit('D', rev_id='D')
 
961
 
 
962
        # Switch to a read lock for this tree.
 
963
        # We still have an addCleanup(tree.unlock) pending
 
964
        tree.unlock()
 
965
        tree.lock_read()
 
966
        return tree
 
967
 
 
968
    def check_delta(self, delta, **kw):
 
969
        """Check the filenames touched by a delta are as expected.
 
970
 
 
971
        Caller only have to pass in the list of files for each part, all
 
972
        unspecified parts are considered empty (and checked as such).
 
973
        """
 
974
        for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
 
975
            # By default we expect an empty list
 
976
            expected = kw.get(n, [])
 
977
            # strip out only the path components
 
978
            got = [x[0] for x in getattr(delta, n)]
 
979
            self.assertEqual(expected, got)
 
980
 
 
981
    def test_tree_with_single_merge(self):
 
982
        """Make sure the tree layout is correct."""
 
983
        tree = self.create_tree_with_single_merge()
 
984
        rev_A_tree = tree.branch.repository.revision_tree('A')
 
985
        rev_B_tree = tree.branch.repository.revision_tree('B')
 
986
        rev_C_tree = tree.branch.repository.revision_tree('C')
 
987
        rev_D_tree = tree.branch.repository.revision_tree('D')
 
988
 
 
989
        self.check_delta(rev_B_tree.changes_from(rev_A_tree),
 
990
                         modified=['f1', 'f3'])
 
991
 
 
992
        self.check_delta(rev_C_tree.changes_from(rev_A_tree),
 
993
                         modified=['f2', 'f3'])
 
994
 
 
995
        self.check_delta(rev_D_tree.changes_from(rev_B_tree),
 
996
                         modified=['f2', 'f3'])
 
997
 
 
998
        self.check_delta(rev_D_tree.changes_from(rev_C_tree),
 
999
                         modified=['f1', 'f3'])
 
1000
 
 
1001
    def assertAllRevisionsForFileID(self, tree, file_id, revisions):
 
1002
        """Ensure _filter_revisions_touching_file_id returns the right values.
 
1003
 
 
1004
        Get the return value from _filter_revisions_touching_file_id and make
 
1005
        sure they are correct.
 
1006
        """
 
1007
        # The api for _filter_revisions_touching_file_id is a little crazy.
 
1008
        # So we do the setup here.
 
1009
        mainline = tree.branch.revision_history()
 
1010
        mainline.insert(0, None)
 
1011
        revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
 
1012
        view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
 
1013
                                                'reverse', True)
 
1014
        actual_revs = log._filter_revisions_touching_file_id(
 
1015
                            tree.branch,
 
1016
                            file_id,
 
1017
                            list(view_revs_iter))
 
1018
        self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
 
1019
 
 
1020
    def test_file_id_f1(self):
 
1021
        tree = self.create_tree_with_single_merge()
 
1022
        # f1 should be marked as modified by revisions A and B
 
1023
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
 
1024
 
 
1025
    def test_file_id_f2(self):
 
1026
        tree = self.create_tree_with_single_merge()
 
1027
        # f2 should be marked as modified by revisions A, C, and D
 
1028
        # because D merged the changes from C.
 
1029
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
 
1030
 
 
1031
    def test_file_id_f3(self):
 
1032
        tree = self.create_tree_with_single_merge()
 
1033
        # f3 should be marked as modified by revisions A, B, C, and D
 
1034
        self.assertAllRevisionsForFileID(tree, 'f3-id', ['D', 'C', 'B', 'A'])
 
1035
 
 
1036
    def test_file_id_with_ghosts(self):
 
1037
        # This is testing bug #209948, where having a ghost would cause
 
1038
        # _filter_revisions_touching_file_id() to fail.
 
1039
        tree = self.create_tree_with_single_merge()
 
1040
        # We need to add a revision, so switch back to a write-locked tree
 
1041
        # (still a single addCleanup(tree.unlock) pending).
 
1042
        tree.unlock()
 
1043
        tree.lock_write()
 
1044
        first_parent = tree.last_revision()
 
1045
        tree.set_parent_ids([first_parent, 'ghost-revision-id'])
 
1046
        self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
 
1047
        tree.commit('commit with a ghost', rev_id='XX')
 
1048
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
 
1049
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
 
1050
 
 
1051
 
 
1052
class TestShowChangedRevisions(tests.TestCaseWithTransport):
 
1053
 
 
1054
    def test_show_changed_revisions_verbose(self):
 
1055
        tree = self.make_branch_and_tree('tree_a')
 
1056
        self.build_tree(['tree_a/foo'])
 
1057
        tree.add('foo')
 
1058
        tree.commit('bar', rev_id='bar-id')
 
1059
        s = self.make_utf8_encoded_stringio()
 
1060
        log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
 
1061
        self.assertContainsRe(s.getvalue(), 'bar')
 
1062
        self.assertNotContainsRe(s.getvalue(), 'foo')
 
1063
 
 
1064
 
 
1065
class TestLogFormatter(tests.TestCase):
 
1066
 
 
1067
    def test_short_committer(self):
 
1068
        rev = revision.Revision('a-id')
 
1069
        rev.committer = 'John Doe <jdoe@example.com>'
 
1070
        lf = log.LogFormatter(None)
 
1071
        self.assertEqual('John Doe', lf.short_committer(rev))
 
1072
        rev.committer = 'John Smith <jsmith@example.com>'
 
1073
        self.assertEqual('John Smith', lf.short_committer(rev))
 
1074
        rev.committer = 'John Smith'
 
1075
        self.assertEqual('John Smith', lf.short_committer(rev))
 
1076
        rev.committer = 'jsmith@example.com'
 
1077
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
 
1078
        rev.committer = '<jsmith@example.com>'
 
1079
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
 
1080
        rev.committer = 'John Smith jsmith@example.com'
 
1081
        self.assertEqual('John Smith', lf.short_committer(rev))
 
1082
 
 
1083
    def test_short_author(self):
 
1084
        rev = revision.Revision('a-id')
 
1085
        rev.committer = 'John Doe <jdoe@example.com>'
 
1086
        lf = log.LogFormatter(None)
 
1087
        self.assertEqual('John Doe', lf.short_author(rev))
 
1088
        rev.properties['author'] = 'John Smith <jsmith@example.com>'
 
1089
        self.assertEqual('John Smith', lf.short_author(rev))
 
1090
        rev.properties['author'] = 'John Smith'
 
1091
        self.assertEqual('John Smith', lf.short_author(rev))
 
1092
        rev.properties['author'] = 'jsmith@example.com'
 
1093
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
 
1094
        rev.properties['author'] = '<jsmith@example.com>'
 
1095
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
 
1096
        rev.properties['author'] = 'John Smith jsmith@example.com'
 
1097
        self.assertEqual('John Smith', lf.short_author(rev))
 
1098
 
 
1099
 
 
1100
class TestReverseByDepth(tests.TestCase):
 
1101
    """Test reverse_by_depth behavior.
 
1102
 
 
1103
    This is used to present revisions in forward (oldest first) order in a nice
 
1104
    layout.
 
1105
 
 
1106
    The tests use lighter revision description to ease reading.
 
1107
    """
 
1108
 
 
1109
    def assertReversed(self, forward, backward):
 
1110
        # Transform the descriptions to suit the API: tests use (revno, depth),
 
1111
        # while the API expects (revid, revno, depth)
 
1112
        def complete_revisions(l):
 
1113
            """Transform the description to suit the API.
 
1114
 
 
1115
            Tests use (revno, depth) whil the API expects (revid, revno, depth).
 
1116
            Since the revid is arbitrary, we just duplicate revno
 
1117
            """
 
1118
            return [ (r, r, d) for r, d in l]
 
1119
        forward = complete_revisions(forward)
 
1120
        backward= complete_revisions(backward)
 
1121
        self.assertEqual(forward, log.reverse_by_depth(backward))
 
1122
 
 
1123
 
 
1124
    def test_mainline_revisions(self):
 
1125
        self.assertReversed([( '1', 0), ('2', 0)],
 
1126
                            [('2', 0), ('1', 0)])
 
1127
 
 
1128
    def test_merged_revisions(self):
 
1129
        self.assertReversed([('1', 0), ('2', 0), ('2.2', 1), ('2.1', 1),],
 
1130
                            [('2', 0), ('2.1', 1), ('2.2', 1), ('1', 0),])
 
1131
    def test_shifted_merged_revisions(self):
 
1132
        """Test irregular layout.
 
1133
 
 
1134
        Requesting revisions touching a file can produce "holes" in the depths.
 
1135
        """
 
1136
        self.assertReversed([('1', 0), ('2', 0), ('1.1', 2), ('1.2', 2),],
 
1137
                            [('2', 0), ('1.2', 2), ('1.1', 2), ('1', 0),])
 
1138
 
 
1139
    def test_merged_without_child_revisions(self):
 
1140
        """Test irregular layout.
 
1141
 
 
1142
        Revision ranges can produce "holes" in the depths.
 
1143
        """
 
1144
        # When a revision of higher depth doesn't follow one of lower depth, we
 
1145
        # assume a lower depth one is virtually there
 
1146
        self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
 
1147
                            [('4', 4), ('3', 3), ('2', 2), ('1', 2),])
 
1148
        # So we get the same order after reversing below even if the original
 
1149
        # revisions are not in the same order.
 
1150
        self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
 
1151
                            [('3', 3), ('4', 4), ('2', 2), ('1', 2),])