/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: Ian Clatworthy
  • Date: 2009-01-22 13:58:18 UTC
  • mto: (3951.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 3952.
  • Revision ID: ian.clatworthy@canonical.com-20090122135818-twftjodatp3cm7xm
review feedback from jam

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