~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_log.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-06-20 01:09:18 UTC
  • mfrom: (3505.1.1 ianc-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20080620010918-64z4xylh1ap5hgyf
Accept user names with @s in URLs (Neil Martinsen-Burrell)

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 log
 
21
from bzrlib.tests import TestCase, TestCaseWithTransport
 
22
from bzrlib.log import (show_log,
 
23
                        get_view_revisions,
 
24
                        LogRevision,
 
25
                        LogFormatter,
 
26
                        LongLogFormatter,
 
27
                        ShortLogFormatter,
 
28
                        LineLogFormatter)
 
29
from bzrlib.branch import Branch
 
30
from bzrlib.errors import (
 
31
    BzrCommandError,
 
32
    InvalidRevisionNumber,
 
33
    )
 
34
from bzrlib.revision import Revision
 
35
from bzrlib.revisionspec import (
 
36
    RevisionInfo,
 
37
    RevisionSpec,
 
38
    )
 
39
 
 
40
 
 
41
class LogCatcher(LogFormatter):
 
42
    """Pull log messages into list rather than displaying them.
 
43
 
 
44
    For ease of testing we save log messages here rather than actually
 
45
    formatting them, so that we can precisely check the result without
 
46
    being too dependent on the exact formatting.
 
47
 
 
48
    We should also test the LogFormatter.
 
49
    """
 
50
 
 
51
    supports_delta = True
 
52
 
 
53
    def __init__(self):
 
54
        super(LogCatcher, self).__init__(to_file=None)
 
55
        self.logs = []
 
56
 
 
57
    def log_revision(self, revision):
 
58
        self.logs.append(revision)
 
59
 
 
60
 
 
61
class TestShowLog(TestCaseWithTransport):
 
62
 
 
63
    def checkDelta(self, delta, **kw):
 
64
        """Check the filenames touched by a delta are as expected."""
 
65
        for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
 
66
            expected = kw.get(n, [])
 
67
            # strip out only the path components
 
68
            got = [x[0] for x in getattr(delta, n)]
 
69
            self.assertEquals(expected, got)
 
70
 
 
71
    def test_cur_revno(self):
 
72
        wt = self.make_branch_and_tree('.')
 
73
        b = wt.branch
 
74
 
 
75
        lf = LogCatcher()
 
76
        wt.commit('empty commit')
 
77
        show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
 
78
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
79
                          start_revision=2, end_revision=1) 
 
80
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
81
                          start_revision=1, end_revision=2) 
 
82
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
83
                          start_revision=0, end_revision=2) 
 
84
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
85
                          start_revision=1, end_revision=0) 
 
86
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
87
                          start_revision=-1, end_revision=1) 
 
88
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
89
                          start_revision=1, end_revision=-1) 
 
90
 
 
91
    def test_simple_log(self):
 
92
        eq = self.assertEquals
 
93
        
 
94
        wt = self.make_branch_and_tree('.')
 
95
        b = wt.branch
 
96
 
 
97
        lf = LogCatcher()
 
98
        show_log(b, lf)
 
99
        # no entries yet
 
100
        eq(lf.logs, [])
 
101
 
 
102
        wt.commit('empty commit')
 
103
        lf = LogCatcher()
 
104
        show_log(b, lf, verbose=True)
 
105
        eq(len(lf.logs), 1)
 
106
        eq(lf.logs[0].revno, '1')
 
107
        eq(lf.logs[0].rev.message, 'empty commit')
 
108
        d = lf.logs[0].delta
 
109
        self.log('log delta: %r' % d)
 
110
        self.checkDelta(d)
 
111
 
 
112
        self.build_tree(['hello'])
 
113
        wt.add('hello')
 
114
        wt.commit('add one file',
 
115
                  committer=u'\u013d\xf3r\xe9m \xcdp\u0161\xfam '
 
116
                            u'<test@example.com>')
 
117
 
 
118
        lf = self.make_utf8_encoded_stringio()
 
119
        # log using regular thing
 
120
        show_log(b, LongLogFormatter(lf))
 
121
        lf.seek(0)
 
122
        for l in lf.readlines():
 
123
            self.log(l)
 
124
 
 
125
        # get log as data structure
 
126
        lf = LogCatcher()
 
127
        show_log(b, lf, verbose=True)
 
128
        eq(len(lf.logs), 2)
 
129
        self.log('log entries:')
 
130
        for logentry in lf.logs:
 
131
            self.log('%4s %s' % (logentry.revno, logentry.rev.message))
 
132
        
 
133
        # first one is most recent
 
134
        logentry = lf.logs[0]
 
135
        eq(logentry.revno, '2')
 
136
        eq(logentry.rev.message, 'add one file')
 
137
        d = logentry.delta
 
138
        self.log('log 2 delta: %r' % d)
 
139
        self.checkDelta(d, added=['hello'])
 
140
        
 
141
        # commit a log message with control characters
 
142
        msg = "All 8-bit chars: " +  ''.join([unichr(x) for x in range(256)])
 
143
        self.log("original commit message: %r", msg)
 
144
        wt.commit(msg)
 
145
        lf = LogCatcher()
 
146
        show_log(b, lf, verbose=True)
 
147
        committed_msg = lf.logs[0].rev.message
 
148
        self.log("escaped commit message: %r", committed_msg)
 
149
        self.assert_(msg != committed_msg)
 
150
        self.assert_(len(committed_msg) > len(msg))
 
151
 
 
152
        # Check that log message with only XML-valid characters isn't
 
153
        # escaped.  As ElementTree apparently does some kind of
 
154
        # newline conversion, neither LF (\x0A) nor CR (\x0D) are
 
155
        # included in the test commit message, even though they are
 
156
        # valid XML 1.0 characters.
 
157
        msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
 
158
        self.log("original commit message: %r", msg)
 
159
        wt.commit(msg)
 
160
        lf = LogCatcher()
 
161
        show_log(b, lf, verbose=True)
 
162
        committed_msg = lf.logs[0].rev.message
 
163
        self.log("escaped commit message: %r", committed_msg)
 
164
        self.assert_(msg == committed_msg)
 
165
 
 
166
    def test_deltas_in_merge_revisions(self):
 
167
        """Check deltas created for both mainline and merge revisions"""
 
168
        eq = self.assertEquals
 
169
        wt = self.make_branch_and_tree('parent')
 
170
        self.build_tree(['parent/file1', 'parent/file2', 'parent/file3'])
 
171
        wt.add('file1')
 
172
        wt.add('file2')
 
173
        wt.commit(message='add file1 and file2')
 
174
        self.run_bzr('branch parent child')
 
175
        os.unlink('child/file1')
 
176
        file('child/file2', 'wb').write('hello\n')
 
177
        self.run_bzr(['commit', '-m', 'remove file1 and modify file2',
 
178
            'child'])
 
179
        os.chdir('parent')
 
180
        self.run_bzr('merge ../child')
 
181
        wt.commit('merge child branch')
 
182
        os.chdir('..')
 
183
        b = wt.branch
 
184
        lf = LogCatcher()
 
185
        lf.supports_merge_revisions = True
 
186
        show_log(b, lf, verbose=True)
 
187
        eq(len(lf.logs),3)
 
188
        logentry = lf.logs[0]
 
189
        eq(logentry.revno, '2')
 
190
        eq(logentry.rev.message, 'merge child branch')
 
191
        d = logentry.delta
 
192
        self.checkDelta(d, removed=['file1'], modified=['file2'])
 
193
        logentry = lf.logs[1]
 
194
        eq(logentry.revno, '1.1.1')
 
195
        eq(logentry.rev.message, 'remove file1 and modify file2')
 
196
        d = logentry.delta
 
197
        self.checkDelta(d, removed=['file1'], modified=['file2'])
 
198
        logentry = lf.logs[2]
 
199
        eq(logentry.revno, '1')
 
200
        eq(logentry.rev.message, 'add file1 and file2')
 
201
        d = logentry.delta
 
202
        self.checkDelta(d, added=['file1', 'file2'])
 
203
 
 
204
    def test_merges_nonsupporting_formatter(self):
 
205
        """Tests that show_log will raise if the formatter doesn't
 
206
        support merge revisions."""
 
207
        wt = self.make_branch_and_memory_tree('.')
 
208
        wt.lock_write()
 
209
        try:
 
210
            wt.add('')
 
211
            wt.commit('rev-1', rev_id='rev-1',
 
212
                      timestamp=1132586655, timezone=36000,
 
213
                      committer='Joe Foo <joe@foo.com>')
 
214
            wt.commit('rev-merged', rev_id='rev-2a',
 
215
                      timestamp=1132586700, timezone=36000,
 
216
                      committer='Joe Foo <joe@foo.com>')
 
217
            wt.set_parent_ids(['rev-1', 'rev-2a'])
 
218
            wt.branch.set_last_revision_info(1, 'rev-1')
 
219
            wt.commit('rev-2', rev_id='rev-2b',
 
220
                      timestamp=1132586800, timezone=36000,
 
221
                      committer='Joe Foo <joe@foo.com>')
 
222
            logfile = self.make_utf8_encoded_stringio()
 
223
            formatter = ShortLogFormatter(to_file=logfile)
 
224
            wtb = wt.branch
 
225
            lf = LogCatcher()
 
226
            revspec = RevisionSpec.from_string('1.1.1')
 
227
            rev = revspec.in_history(wtb)
 
228
            self.assertRaises(BzrCommandError, show_log, wtb, lf,
 
229
                              start_revision=rev, end_revision=rev)
 
230
        finally:
 
231
            wt.unlock()
 
232
 
 
233
 
 
234
def make_commits_with_trailing_newlines(wt):
 
235
    """Helper method for LogFormatter tests"""    
 
236
    b = wt.branch
 
237
    b.nick='test'
 
238
    open('a', 'wb').write('hello moto\n')
 
239
    wt.add('a')
 
240
    wt.commit('simple log message', rev_id='a1',
 
241
              timestamp=1132586655.459960938, timezone=-6*3600,
 
242
              committer='Joe Foo <joe@foo.com>')
 
243
    open('b', 'wb').write('goodbye\n')
 
244
    wt.add('b')
 
245
    wt.commit('multiline\nlog\nmessage\n', rev_id='a2',
 
246
              timestamp=1132586842.411175966, timezone=-6*3600,
 
247
              committer='Joe Foo <joe@foo.com>',
 
248
              author='Joe Bar <joe@bar.com>')
 
249
 
 
250
    open('c', 'wb').write('just another manic monday\n')
 
251
    wt.add('c')
 
252
    wt.commit('single line with trailing newline\n', rev_id='a3',
 
253
              timestamp=1132587176.835228920, timezone=-6*3600,
 
254
              committer = 'Joe Foo <joe@foo.com>')
 
255
    return b
 
256
 
 
257
 
 
258
def normalize_log(log):
 
259
    """Replaces the variable lines of logs with fixed lines"""
 
260
    author = 'author: Dolor Sit <test@example.com>'
 
261
    committer = 'committer: Lorem Ipsum <test@example.com>'
 
262
    lines = log.splitlines(True)
 
263
    for idx,line in enumerate(lines):
 
264
        stripped_line = line.lstrip()
 
265
        indent = ' ' * (len(line) - len(stripped_line))
 
266
        if stripped_line.startswith('author:'):
 
267
            lines[idx] = indent + author + '\n'
 
268
        elif stripped_line.startswith('committer:'):
 
269
            lines[idx] = indent + committer + '\n'
 
270
        elif stripped_line.startswith('timestamp:'):
 
271
            lines[idx] = indent + 'timestamp: Just now\n'
 
272
    return ''.join(lines)
 
273
 
 
274
 
 
275
class TestShortLogFormatter(TestCaseWithTransport):
 
276
 
 
277
    def test_trailing_newlines(self):
 
278
        wt = self.make_branch_and_tree('.')
 
279
        b = make_commits_with_trailing_newlines(wt)
 
280
        sio = self.make_utf8_encoded_stringio()
 
281
        lf = ShortLogFormatter(to_file=sio)
 
282
        show_log(b, lf)
 
283
        self.assertEqualDiff(sio.getvalue(), """\
 
284
    3 Joe Foo\t2005-11-21
 
285
      single line with trailing newline
 
286
 
 
287
    2 Joe Bar\t2005-11-21
 
288
      multiline
 
289
      log
 
290
      message
 
291
 
 
292
    1 Joe Foo\t2005-11-21
 
293
      simple log message
 
294
 
 
295
""")
 
296
 
 
297
    def test_short_log_with_merges(self):
 
298
        wt = self.make_branch_and_memory_tree('.')
 
299
        wt.lock_write()
 
300
        try:
 
301
            wt.add('')
 
302
            wt.commit('rev-1', rev_id='rev-1',
 
303
                      timestamp=1132586655, timezone=36000,
 
304
                      committer='Joe Foo <joe@foo.com>')
 
305
            wt.commit('rev-merged', rev_id='rev-2a',
 
306
                      timestamp=1132586700, timezone=36000,
 
307
                      committer='Joe Foo <joe@foo.com>')
 
308
            wt.set_parent_ids(['rev-1', 'rev-2a'])
 
309
            wt.branch.set_last_revision_info(1, 'rev-1')
 
310
            wt.commit('rev-2', rev_id='rev-2b',
 
311
                      timestamp=1132586800, timezone=36000,
 
312
                      committer='Joe Foo <joe@foo.com>')
 
313
            logfile = self.make_utf8_encoded_stringio()
 
314
            formatter = ShortLogFormatter(to_file=logfile)
 
315
            show_log(wt.branch, formatter)
 
316
            self.assertEqualDiff(logfile.getvalue(), """\
 
317
    2 Joe Foo\t2005-11-22 [merge]
 
318
      rev-2
 
319
 
 
320
    1 Joe Foo\t2005-11-22
 
321
      rev-1
 
322
 
 
323
""")
 
324
        finally:
 
325
            wt.unlock()
 
326
 
 
327
    def test_short_log_single_merge_revision(self):
 
328
        wt = self.make_branch_and_memory_tree('.')
 
329
        wt.lock_write()
 
330
        try:
 
331
            wt.add('')
 
332
            wt.commit('rev-1', rev_id='rev-1',
 
333
                      timestamp=1132586655, timezone=36000,
 
334
                      committer='Joe Foo <joe@foo.com>')
 
335
            wt.commit('rev-merged', rev_id='rev-2a',
 
336
                      timestamp=1132586700, timezone=36000,
 
337
                      committer='Joe Foo <joe@foo.com>')
 
338
            wt.set_parent_ids(['rev-1', 'rev-2a'])
 
339
            wt.branch.set_last_revision_info(1, 'rev-1')
 
340
            wt.commit('rev-2', rev_id='rev-2b',
 
341
                      timestamp=1132586800, timezone=36000,
 
342
                      committer='Joe Foo <joe@foo.com>')
 
343
            logfile = self.make_utf8_encoded_stringio()
 
344
            formatter = ShortLogFormatter(to_file=logfile)
 
345
            revspec = RevisionSpec.from_string('1.1.1')
 
346
            wtb = wt.branch
 
347
            rev = revspec.in_history(wtb)
 
348
            show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
349
            self.assertEqualDiff(logfile.getvalue(), """\
 
350
1.1.1 Joe Foo\t2005-11-22
 
351
      rev-merged
 
352
 
 
353
""")
 
354
        finally:
 
355
            wt.unlock()
 
356
 
 
357
 
 
358
class TestLongLogFormatter(TestCaseWithTransport):
 
359
 
 
360
    def test_verbose_log(self):
 
361
        """Verbose log includes changed files
 
362
        
 
363
        bug #4676
 
364
        """
 
365
        wt = self.make_branch_and_tree('.')
 
366
        b = wt.branch
 
367
        self.build_tree(['a'])
 
368
        wt.add('a')
 
369
        # XXX: why does a longer nick show up?
 
370
        b.nick = 'test_verbose_log'
 
371
        wt.commit(message='add a', 
 
372
                  timestamp=1132711707, 
 
373
                  timezone=36000,
 
374
                  committer='Lorem Ipsum <test@example.com>')
 
375
        logfile = file('out.tmp', 'w+')
 
376
        formatter = LongLogFormatter(to_file=logfile)
 
377
        show_log(b, formatter, verbose=True)
 
378
        logfile.flush()
 
379
        logfile.seek(0)
 
380
        log_contents = logfile.read()
 
381
        self.assertEqualDiff(log_contents, '''\
 
382
------------------------------------------------------------
 
383
revno: 1
 
384
committer: Lorem Ipsum <test@example.com>
 
385
branch nick: test_verbose_log
 
386
timestamp: Wed 2005-11-23 12:08:27 +1000
 
387
message:
 
388
  add a
 
389
added:
 
390
  a
 
391
''')
 
392
 
 
393
    def test_merges_are_indented_by_level(self):
 
394
        wt = self.make_branch_and_tree('parent')
 
395
        wt.commit('first post')
 
396
        self.run_bzr('branch parent child')
 
397
        self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
 
398
        self.run_bzr('branch child smallerchild')
 
399
        self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
 
400
            'smallerchild'])
 
401
        os.chdir('child')
 
402
        self.run_bzr('merge ../smallerchild')
 
403
        self.run_bzr(['commit', '-m', 'merge branch 2'])
 
404
        os.chdir('../parent')
 
405
        self.run_bzr('merge ../child')
 
406
        wt.commit('merge branch 1')
 
407
        b = wt.branch
 
408
        sio = self.make_utf8_encoded_stringio()
 
409
        lf = LongLogFormatter(to_file=sio)
 
410
        show_log(b, lf, verbose=True)
 
411
        log = normalize_log(sio.getvalue())
 
412
        self.assertEqualDiff(log, """\
 
413
------------------------------------------------------------
 
414
revno: 2
 
415
committer: Lorem Ipsum <test@example.com>
 
416
branch nick: parent
 
417
timestamp: Just now
 
418
message:
 
419
  merge branch 1
 
420
    ------------------------------------------------------------
 
421
    revno: 1.1.2
 
422
    committer: Lorem Ipsum <test@example.com>
 
423
    branch nick: child
 
424
    timestamp: Just now
 
425
    message:
 
426
      merge branch 2
 
427
        ------------------------------------------------------------
 
428
        revno: 1.2.1
 
429
        committer: Lorem Ipsum <test@example.com>
 
430
        branch nick: smallerchild
 
431
        timestamp: Just now
 
432
        message:
 
433
          branch 2
 
434
    ------------------------------------------------------------
 
435
    revno: 1.1.1
 
436
    committer: Lorem Ipsum <test@example.com>
 
437
    branch nick: child
 
438
    timestamp: Just now
 
439
    message:
 
440
      branch 1
 
441
------------------------------------------------------------
 
442
revno: 1
 
443
committer: Lorem Ipsum <test@example.com>
 
444
branch nick: parent
 
445
timestamp: Just now
 
446
message:
 
447
  first post
 
448
""")
 
449
 
 
450
    def test_verbose_merge_revisions_contain_deltas(self):
 
451
        wt = self.make_branch_and_tree('parent')
 
452
        self.build_tree(['parent/f1', 'parent/f2'])
 
453
        wt.add(['f1','f2'])
 
454
        wt.commit('first post')
 
455
        self.run_bzr('branch parent child')
 
456
        os.unlink('child/f1')
 
457
        file('child/f2', 'wb').write('hello\n')
 
458
        self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
 
459
            'child'])
 
460
        os.chdir('parent')
 
461
        self.run_bzr('merge ../child')
 
462
        wt.commit('merge branch 1')
 
463
        b = wt.branch
 
464
        sio = self.make_utf8_encoded_stringio()
 
465
        lf = LongLogFormatter(to_file=sio)
 
466
        show_log(b, lf, verbose=True)
 
467
        log = normalize_log(sio.getvalue())
 
468
        self.assertEqualDiff(log, """\
 
469
------------------------------------------------------------
 
470
revno: 2
 
471
committer: Lorem Ipsum <test@example.com>
 
472
branch nick: parent
 
473
timestamp: Just now
 
474
message:
 
475
  merge branch 1
 
476
removed:
 
477
  f1
 
478
modified:
 
479
  f2
 
480
    ------------------------------------------------------------
 
481
    revno: 1.1.1
 
482
    committer: Lorem Ipsum <test@example.com>
 
483
    branch nick: child
 
484
    timestamp: Just now
 
485
    message:
 
486
      removed f1 and modified f2
 
487
    removed:
 
488
      f1
 
489
    modified:
 
490
      f2
 
491
------------------------------------------------------------
 
492
revno: 1
 
493
committer: Lorem Ipsum <test@example.com>
 
494
branch nick: parent
 
495
timestamp: Just now
 
496
message:
 
497
  first post
 
498
added:
 
499
  f1
 
500
  f2
 
501
""")
 
502
 
 
503
    def test_trailing_newlines(self):
 
504
        wt = self.make_branch_and_tree('.')
 
505
        b = make_commits_with_trailing_newlines(wt)
 
506
        sio = self.make_utf8_encoded_stringio()
 
507
        lf = LongLogFormatter(to_file=sio)
 
508
        show_log(b, lf)
 
509
        self.assertEqualDiff(sio.getvalue(), """\
 
510
------------------------------------------------------------
 
511
revno: 3
 
512
committer: Joe Foo <joe@foo.com>
 
513
branch nick: test
 
514
timestamp: Mon 2005-11-21 09:32:56 -0600
 
515
message:
 
516
  single line with trailing newline
 
517
------------------------------------------------------------
 
518
revno: 2
 
519
author: Joe Bar <joe@bar.com>
 
520
committer: Joe Foo <joe@foo.com>
 
521
branch nick: test
 
522
timestamp: Mon 2005-11-21 09:27:22 -0600
 
523
message:
 
524
  multiline
 
525
  log
 
526
  message
 
527
------------------------------------------------------------
 
528
revno: 1
 
529
committer: Joe Foo <joe@foo.com>
 
530
branch nick: test
 
531
timestamp: Mon 2005-11-21 09:24:15 -0600
 
532
message:
 
533
  simple log message
 
534
""")
 
535
 
 
536
    def test_author_in_log(self):
 
537
        """Log includes the author name if it's set in
 
538
        the revision properties
 
539
        """
 
540
        wt = self.make_branch_and_tree('.')
 
541
        b = wt.branch
 
542
        self.build_tree(['a'])
 
543
        wt.add('a')
 
544
        b.nick = 'test_author_log'
 
545
        wt.commit(message='add a',
 
546
                  timestamp=1132711707,
 
547
                  timezone=36000,
 
548
                  committer='Lorem Ipsum <test@example.com>',
 
549
                  author='John Doe <jdoe@example.com>')
 
550
        sio = StringIO()
 
551
        formatter = LongLogFormatter(to_file=sio)
 
552
        show_log(b, formatter)
 
553
        self.assertEqualDiff(sio.getvalue(), '''\
 
554
------------------------------------------------------------
 
555
revno: 1
 
556
author: John Doe <jdoe@example.com>
 
557
committer: Lorem Ipsum <test@example.com>
 
558
branch nick: test_author_log
 
559
timestamp: Wed 2005-11-23 12:08:27 +1000
 
560
message:
 
561
  add a
 
562
''')
 
563
 
 
564
 
 
565
 
 
566
class TestLineLogFormatter(TestCaseWithTransport):
 
567
 
 
568
    def test_line_log(self):
 
569
        """Line log should show revno
 
570
        
 
571
        bug #5162
 
572
        """
 
573
        wt = self.make_branch_and_tree('.')
 
574
        b = wt.branch
 
575
        self.build_tree(['a'])
 
576
        wt.add('a')
 
577
        b.nick = 'test-line-log'
 
578
        wt.commit(message='add a', 
 
579
                  timestamp=1132711707, 
 
580
                  timezone=36000,
 
581
                  committer='Line-Log-Formatter Tester <test@line.log>')
 
582
        logfile = file('out.tmp', 'w+')
 
583
        formatter = LineLogFormatter(to_file=logfile)
 
584
        show_log(b, formatter)
 
585
        logfile.flush()
 
586
        logfile.seek(0)
 
587
        log_contents = logfile.read()
 
588
        self.assertEqualDiff(log_contents,
 
589
            '1: Line-Log-Formatte... 2005-11-23 add a\n')
 
590
 
 
591
    def test_trailing_newlines(self):
 
592
        wt = self.make_branch_and_tree('.')
 
593
        b = make_commits_with_trailing_newlines(wt)
 
594
        sio = self.make_utf8_encoded_stringio()
 
595
        lf = LineLogFormatter(to_file=sio)
 
596
        show_log(b, lf)
 
597
        self.assertEqualDiff(sio.getvalue(), """\
 
598
3: Joe Foo 2005-11-21 single line with trailing newline
 
599
2: Joe Bar 2005-11-21 multiline
 
600
1: Joe Foo 2005-11-21 simple log message
 
601
""")
 
602
 
 
603
    def test_line_log_single_merge_revision(self):
 
604
        wt = self.make_branch_and_memory_tree('.')
 
605
        wt.lock_write()
 
606
        try:
 
607
            wt.add('')
 
608
            wt.commit('rev-1', rev_id='rev-1',
 
609
                      timestamp=1132586655, timezone=36000,
 
610
                      committer='Joe Foo <joe@foo.com>')
 
611
            wt.commit('rev-merged', rev_id='rev-2a',
 
612
                      timestamp=1132586700, timezone=36000,
 
613
                      committer='Joe Foo <joe@foo.com>')
 
614
            wt.set_parent_ids(['rev-1', 'rev-2a'])
 
615
            wt.branch.set_last_revision_info(1, 'rev-1')
 
616
            wt.commit('rev-2', rev_id='rev-2b',
 
617
                      timestamp=1132586800, timezone=36000,
 
618
                      committer='Joe Foo <joe@foo.com>')
 
619
            logfile = self.make_utf8_encoded_stringio()
 
620
            formatter = LineLogFormatter(to_file=logfile)
 
621
            revspec = RevisionSpec.from_string('1.1.1')
 
622
            wtb = wt.branch
 
623
            rev = revspec.in_history(wtb)
 
624
            show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
625
            self.assertEqualDiff(logfile.getvalue(), """\
 
626
1.1.1: Joe Foo 2005-11-22 rev-merged
 
627
""")
 
628
        finally:
 
629
            wt.unlock()
 
630
 
 
631
 
 
632
 
 
633
class TestGetViewRevisions(TestCaseWithTransport):
 
634
 
 
635
    def make_tree_with_commits(self):
 
636
        """Create a tree with well-known revision ids"""
 
637
        wt = self.make_branch_and_tree('tree1')
 
638
        wt.commit('commit one', rev_id='1')
 
639
        wt.commit('commit two', rev_id='2')
 
640
        wt.commit('commit three', rev_id='3')
 
641
        mainline_revs = [None, '1', '2', '3']
 
642
        rev_nos = {'1': 1, '2': 2, '3': 3}
 
643
        return mainline_revs, rev_nos, wt
 
644
 
 
645
    def make_tree_with_merges(self):
 
646
        """Create a tree with well-known revision ids and a merge"""
 
647
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
 
648
        tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
 
649
        tree2.commit('four-a', rev_id='4a')
 
650
        wt.merge_from_branch(tree2.branch)
 
651
        wt.commit('four-b', rev_id='4b')
 
652
        mainline_revs.append('4b')
 
653
        rev_nos['4b'] = 4
 
654
        # 4a: 3.1.1
 
655
        return mainline_revs, rev_nos, wt
 
656
 
 
657
    def make_tree_with_many_merges(self):
 
658
        """Create a tree with well-known revision ids"""
 
659
        wt = self.make_branch_and_tree('tree1')
 
660
        wt.commit('commit one', rev_id='1')
 
661
        wt.commit('commit two', rev_id='2')
 
662
        tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
 
663
        tree3.commit('commit three a', rev_id='3a')
 
664
        tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
 
665
        tree2.merge_from_branch(tree3.branch)
 
666
        tree2.commit('commit three b', rev_id='3b')
 
667
        wt.merge_from_branch(tree2.branch)
 
668
        wt.commit('commit three c', rev_id='3c')
 
669
        tree2.commit('four-a', rev_id='4a')
 
670
        wt.merge_from_branch(tree2.branch)
 
671
        wt.commit('four-b', rev_id='4b')
 
672
        mainline_revs = [None, '1', '2', '3c', '4b']
 
673
        rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
 
674
        full_rev_nos_for_reference = {
 
675
            '1': '1',
 
676
            '2': '2',
 
677
            '3a': '2.1.1', #first commit tree 3
 
678
            '3b': '2.2.1', # first commit tree 2
 
679
            '3c': '3', #merges 3b to main
 
680
            '4a': '2.2.2', # second commit tree 2
 
681
            '4b': '4', # merges 4a to main
 
682
            }
 
683
        return mainline_revs, rev_nos, wt
 
684
 
 
685
    def test_get_view_revisions_forward(self):
 
686
        """Test the get_view_revisions method"""
 
687
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
 
688
        wt.lock_read()
 
689
        self.addCleanup(wt.unlock)
 
690
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
691
                                            'forward'))
 
692
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
 
693
            revisions)
 
694
        revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
695
                                             'forward', include_merges=False))
 
696
        self.assertEqual(revisions, revisions2)
 
697
 
 
698
    def test_get_view_revisions_reverse(self):
 
699
        """Test the get_view_revisions with reverse"""
 
700
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
 
701
        wt.lock_read()
 
702
        self.addCleanup(wt.unlock)
 
703
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
704
                                            'reverse'))
 
705
        self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
 
706
            revisions)
 
707
        revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
708
                                             'reverse', include_merges=False))
 
709
        self.assertEqual(revisions, revisions2)
 
710
 
 
711
    def test_get_view_revisions_merge(self):
 
712
        """Test get_view_revisions when there are merges"""
 
713
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
 
714
        wt.lock_read()
 
715
        self.addCleanup(wt.unlock)
 
716
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
717
                                            'forward'))
 
718
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
 
719
            ('4b', '4', 0), ('4a', '3.1.1', 1)],
 
720
            revisions)
 
721
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
722
                                             'forward', include_merges=False))
 
723
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
 
724
            ('4b', '4', 0)],
 
725
            revisions)
 
726
 
 
727
    def test_get_view_revisions_merge_reverse(self):
 
728
        """Test get_view_revisions in reverse when there are merges"""
 
729
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
 
730
        wt.lock_read()
 
731
        self.addCleanup(wt.unlock)
 
732
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
733
                                            'reverse'))
 
734
        self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
 
735
            ('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
 
736
            revisions)
 
737
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
738
                                             'reverse', include_merges=False))
 
739
        self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
 
740
            ('1', '1', 0)],
 
741
            revisions)
 
742
 
 
743
    def test_get_view_revisions_merge2(self):
 
744
        """Test get_view_revisions when there are merges"""
 
745
        mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
 
746
        wt.lock_read()
 
747
        self.addCleanup(wt.unlock)
 
748
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
749
                                            'forward'))
 
750
        expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
 
751
            ('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
 
752
            ('4a', '2.2.2', 1)]
 
753
        self.assertEqual(expected, revisions)
 
754
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
 
755
                                             'forward', include_merges=False))
 
756
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
 
757
            ('4b', '4', 0)],
 
758
            revisions)
 
759
 
 
760
 
 
761
class TestGetRevisionsTouchingFileID(TestCaseWithTransport):
 
762
 
 
763
    def create_tree_with_single_merge(self):
 
764
        """Create a branch with a moderate layout.
 
765
 
 
766
        The revision graph looks like:
 
767
 
 
768
           A
 
769
           |\
 
770
           B C
 
771
           |/
 
772
           D
 
773
 
 
774
        In this graph, A introduced files f1 and f2 and f3.
 
775
        B modifies f1 and f3, and C modifies f2 and f3.
 
776
        D merges the changes from B and C and resolves the conflict for f3.
 
777
        """
 
778
        # TODO: jam 20070218 This seems like it could really be done
 
779
        #       with make_branch_and_memory_tree() if we could just
 
780
        #       create the content of those files.
 
781
        # TODO: jam 20070218 Another alternative is that we would really
 
782
        #       like to only create this tree 1 time for all tests that
 
783
        #       use it. Since 'log' only uses the tree in a readonly
 
784
        #       fashion, it seems a shame to regenerate an identical
 
785
        #       tree for each test.
 
786
        tree = self.make_branch_and_tree('tree')
 
787
        tree.lock_write()
 
788
        self.addCleanup(tree.unlock)
 
789
 
 
790
        self.build_tree_contents([('tree/f1', 'A\n'),
 
791
                                  ('tree/f2', 'A\n'),
 
792
                                  ('tree/f3', 'A\n'),
 
793
                                 ])
 
794
        tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
 
795
        tree.commit('A', rev_id='A')
 
796
 
 
797
        self.build_tree_contents([('tree/f2', 'A\nC\n'),
 
798
                                  ('tree/f3', 'A\nC\n'),
 
799
                                 ])
 
800
        tree.commit('C', rev_id='C')
 
801
        # Revert back to A to build the other history.
 
802
        tree.set_last_revision('A')
 
803
        tree.branch.set_last_revision_info(1, 'A')
 
804
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
 
805
                                  ('tree/f2', 'A\n'),
 
806
                                  ('tree/f3', 'A\nB\n'),
 
807
                                 ])
 
808
        tree.commit('B', rev_id='B')
 
809
        tree.set_parent_ids(['B', 'C'])
 
810
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
 
811
                                  ('tree/f2', 'A\nC\n'),
 
812
                                  ('tree/f3', 'A\nB\nC\n'),
 
813
                                 ])
 
814
        tree.commit('D', rev_id='D')
 
815
 
 
816
        # Switch to a read lock for this tree.
 
817
        # We still have addCleanup(unlock)
 
818
        tree.unlock()
 
819
        tree.lock_read()
 
820
        return tree
 
821
 
 
822
    def test_tree_with_single_merge(self):
 
823
        """Make sure the tree layout is correct."""
 
824
        tree = self.create_tree_with_single_merge()
 
825
        rev_A_tree = tree.branch.repository.revision_tree('A')
 
826
        rev_B_tree = tree.branch.repository.revision_tree('B')
 
827
 
 
828
        f1_changed = (u'f1', 'f1-id', 'file', True, False)
 
829
        f2_changed = (u'f2', 'f2-id', 'file', True, False)
 
830
        f3_changed = (u'f3', 'f3-id', 'file', True, False)
 
831
 
 
832
        delta = rev_B_tree.changes_from(rev_A_tree)
 
833
        self.assertEqual([f1_changed, f3_changed], delta.modified)
 
834
        self.assertEqual([], delta.renamed)
 
835
        self.assertEqual([], delta.added)
 
836
        self.assertEqual([], delta.removed)
 
837
 
 
838
        rev_C_tree = tree.branch.repository.revision_tree('C')
 
839
        delta = rev_C_tree.changes_from(rev_A_tree)
 
840
        self.assertEqual([f2_changed, f3_changed], delta.modified)
 
841
        self.assertEqual([], delta.renamed)
 
842
        self.assertEqual([], delta.added)
 
843
        self.assertEqual([], delta.removed)
 
844
 
 
845
        rev_D_tree = tree.branch.repository.revision_tree('D')
 
846
        delta = rev_D_tree.changes_from(rev_B_tree)
 
847
        self.assertEqual([f2_changed, f3_changed], delta.modified)
 
848
        self.assertEqual([], delta.renamed)
 
849
        self.assertEqual([], delta.added)
 
850
        self.assertEqual([], delta.removed)
 
851
 
 
852
        delta = rev_D_tree.changes_from(rev_C_tree)
 
853
        self.assertEqual([f1_changed, f3_changed], delta.modified)
 
854
        self.assertEqual([], delta.renamed)
 
855
        self.assertEqual([], delta.added)
 
856
        self.assertEqual([], delta.removed)
 
857
 
 
858
    def assertAllRevisionsForFileID(self, tree, file_id, revisions):
 
859
        """Make sure _filter_revisions_touching_file_id returns the right values.
 
860
 
 
861
        Get the return value from _filter_revisions_touching_file_id and make
 
862
        sure they are correct.
 
863
        """
 
864
        # The api for _get_revisions_touching_file_id is a little crazy,
 
865
        # So we do the setup here.
 
866
        mainline = tree.branch.revision_history()
 
867
        mainline.insert(0, None)
 
868
        revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
 
869
        view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
 
870
                                                'reverse', True)
 
871
        actual_revs = log._filter_revisions_touching_file_id(
 
872
                            tree.branch, 
 
873
                            file_id,
 
874
                            mainline,
 
875
                            list(view_revs_iter))
 
876
        self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
 
877
 
 
878
    def test_file_id_f1(self):
 
879
        tree = self.create_tree_with_single_merge()
 
880
        # f1 should be marked as modified by revisions A and B
 
881
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
 
882
 
 
883
    def test_file_id_f2(self):
 
884
        tree = self.create_tree_with_single_merge()
 
885
        # f2 should be marked as modified by revisions A, C, and D
 
886
        # because D merged the changes from C.
 
887
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
 
888
 
 
889
    def test_file_id_f3(self):
 
890
        tree = self.create_tree_with_single_merge()
 
891
        # f3 should be marked as modified by revisions A, B, C, and D
 
892
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
 
893
 
 
894
    def test_file_id_with_ghosts(self):
 
895
        # This is testing bug #209948, where having a ghost would cause
 
896
        # _filter_revisions_touching_file_id() to fail.
 
897
        tree = self.create_tree_with_single_merge()
 
898
        # We need to add a revision, so switch back to a write-locked tree
 
899
        tree.unlock()
 
900
        tree.lock_write()
 
901
        first_parent = tree.last_revision()
 
902
        tree.set_parent_ids([first_parent, 'ghost-revision-id'])
 
903
        self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
 
904
        tree.commit('commit with a ghost', rev_id='XX')
 
905
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
 
906
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
 
907
 
 
908
 
 
909
class TestShowChangedRevisions(TestCaseWithTransport):
 
910
 
 
911
    def test_show_changed_revisions_verbose(self):
 
912
        tree = self.make_branch_and_tree('tree_a')
 
913
        self.build_tree(['tree_a/foo'])
 
914
        tree.add('foo')
 
915
        tree.commit('bar', rev_id='bar-id')
 
916
        s = self.make_utf8_encoded_stringio()
 
917
        log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
 
918
        self.assertContainsRe(s.getvalue(), 'bar')
 
919
        self.assertNotContainsRe(s.getvalue(), 'foo')
 
920
 
 
921
 
 
922
class TestLogFormatter(TestCase):
 
923
 
 
924
    def test_short_committer(self):
 
925
        rev = Revision('a-id')
 
926
        rev.committer = 'John Doe <jdoe@example.com>'
 
927
        lf = LogFormatter(None)
 
928
        self.assertEqual('John Doe', lf.short_committer(rev))
 
929
        rev.committer = 'John Smith <jsmith@example.com>'
 
930
        self.assertEqual('John Smith', lf.short_committer(rev))
 
931
        rev.committer = 'John Smith'
 
932
        self.assertEqual('John Smith', lf.short_committer(rev))
 
933
        rev.committer = 'jsmith@example.com'
 
934
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
 
935
        rev.committer = '<jsmith@example.com>'
 
936
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
 
937
        rev.committer = 'John Smith jsmith@example.com'
 
938
        self.assertEqual('John Smith', lf.short_committer(rev))
 
939
 
 
940
    def test_short_author(self):
 
941
        rev = Revision('a-id')
 
942
        rev.committer = 'John Doe <jdoe@example.com>'
 
943
        lf = LogFormatter(None)
 
944
        self.assertEqual('John Doe', lf.short_author(rev))
 
945
        rev.properties['author'] = 'John Smith <jsmith@example.com>'
 
946
        self.assertEqual('John Smith', lf.short_author(rev))
 
947
        rev.properties['author'] = 'John Smith'
 
948
        self.assertEqual('John Smith', lf.short_author(rev))
 
949
        rev.properties['author'] = 'jsmith@example.com'
 
950
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
 
951
        rev.properties['author'] = '<jsmith@example.com>'
 
952
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
 
953
        rev.properties['author'] = 'John Smith jsmith@example.com'
 
954
        self.assertEqual('John Smith', lf.short_author(rev))