~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: 2009-04-07 13:08:21 UTC
  • mfrom: (4222.2.12 ui-username)
  • Revision ID: pqm@pqm.ubuntu.com-20090407130821-e4wi39x60alhpnr4
(Jelmer) Add UIFactory.get_username.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Canonical Ltd
2
 
# -*- coding: utf-8 -*-
3
 
# vim: encoding=utf-8
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
4
2
#
5
3
# This program is free software; you can redistribute it and/or modify
6
4
# it under the terms of the GNU General Public License as published by
14
12
#
15
13
# You should have received a copy of the GNU General Public License
16
14
# along with this program; if not, write to the Free Software
17
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
16
 
19
17
import os
20
18
from cStringIO import StringIO
21
19
 
22
 
from bzrlib.tests import BzrTestBase, TestCaseWithTransport
23
 
from bzrlib.log import (show_log, 
24
 
                        get_view_revisions, 
25
 
                        LogFormatter, 
26
 
                        LongLogFormatter, 
27
 
                        ShortLogFormatter, 
28
 
                        LineLogFormatter)
29
 
from bzrlib.branch import Branch
30
 
from bzrlib.errors import InvalidRevisionNumber
31
 
 
32
 
 
33
 
class _LogEntry(object):
34
 
    # should probably move into bzrlib.log?
35
 
    pass
36
 
 
37
 
 
38
 
class LogCatcher(LogFormatter):
 
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):
39
46
    """Pull log messages into list rather than displaying them.
40
47
 
41
48
    For ease of testing we save log messages here rather than actually
44
51
 
45
52
    We should also test the LogFormatter.
46
53
    """
 
54
 
 
55
    supports_delta = True
 
56
 
47
57
    def __init__(self):
48
58
        super(LogCatcher, self).__init__(to_file=None)
49
59
        self.logs = []
50
60
 
51
 
    def show(self, revno, rev, delta):
52
 
        le = _LogEntry()
53
 
        le.revno = revno
54
 
        le.rev = rev
55
 
        le.delta = delta
56
 
        self.logs.append(le)
57
 
 
58
 
 
59
 
class SimpleLogTest(TestCaseWithTransport):
 
61
    def log_revision(self, revision):
 
62
        self.logs.append(revision)
 
63
 
 
64
 
 
65
class TestShowLog(tests.TestCaseWithTransport):
60
66
 
61
67
    def checkDelta(self, delta, **kw):
62
 
        """Check the filenames touched by a delta are as expected."""
 
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
        """
63
73
        for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
 
74
            # By default we expect an empty list
64
75
            expected = kw.get(n, [])
65
 
 
66
 
            # tests are written with unix paths; fix them up for windows
67
 
            #if os.sep != '/':
68
 
            #    expected = [x.replace('/', os.sep) for x in expected]
69
 
 
70
76
            # strip out only the path components
71
77
            got = [x[0] for x in getattr(delta, n)]
72
 
            self.assertEquals(expected, got)
 
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)
73
85
 
74
86
    def test_cur_revno(self):
75
87
        wt = self.make_branch_and_tree('.')
77
89
 
78
90
        lf = LogCatcher()
79
91
        wt.commit('empty commit')
80
 
        show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
81
 
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
82
 
                          start_revision=2, end_revision=1) 
83
 
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
84
 
                          start_revision=1, end_revision=2) 
85
 
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
86
 
                          start_revision=0, end_revision=2) 
87
 
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
88
 
                          start_revision=1, end_revision=0) 
89
 
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
90
 
                          start_revision=-1, end_revision=1) 
91
 
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
92
 
                          start_revision=1, end_revision=-1) 
93
 
 
94
 
    def test_simple_log(self):
95
 
        eq = self.assertEquals
96
 
        
 
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):
97
104
        wt = self.make_branch_and_tree('.')
98
 
        b = wt.branch
99
105
 
100
106
        lf = LogCatcher()
101
 
        show_log(b, lf)
 
107
        log.show_log(wt.branch, lf)
102
108
        # no entries yet
103
 
        eq(lf.logs, [])
 
109
        self.assertEqual([], lf.logs)
 
110
 
 
111
    def test_empty_commit(self):
 
112
        wt = self.make_branch_and_tree('.')
104
113
 
105
114
        wt.commit('empty commit')
106
115
        lf = LogCatcher()
107
 
        show_log(b, lf, verbose=True)
108
 
        eq(len(lf.logs), 1)
109
 
        eq(lf.logs[0].revno, '1')
110
 
        eq(lf.logs[0].rev.message, 'empty commit')
111
 
        d = lf.logs[0].delta
112
 
        self.log('log delta: %r' % d)
113
 
        self.checkDelta(d)
 
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)
114
121
 
 
122
    def test_simple_commit(self):
 
123
        wt = self.make_branch_and_tree('.')
 
124
        wt.commit('empty commit')
115
125
        self.build_tree(['hello'])
116
126
        wt.add('hello')
117
 
        wt.commit('add one file')
118
 
 
119
 
        lf = StringIO()
120
 
        # log using regular thing
121
 
        show_log(b, LongLogFormatter(lf))
122
 
        lf.seek(0)
123
 
        for l in lf.readlines():
124
 
            self.log(l)
125
 
 
126
 
        # get log as data structure
 
127
        wt.commit('add one file',
 
128
                  committer=u'\u013d\xf3r\xe9m \xcdp\u0161\xfam '
 
129
                            u'<test@example.com>')
127
130
        lf = LogCatcher()
128
 
        show_log(b, lf, verbose=True)
129
 
        eq(len(lf.logs), 2)
130
 
        self.log('log entries:')
131
 
        for logentry in lf.logs:
132
 
            self.log('%4s %s' % (logentry.revno, logentry.rev.message))
133
 
        
 
131
        log.show_log(wt.branch, lf, verbose=True)
 
132
        self.assertEqual(2, len(lf.logs))
134
133
        # first one is most recent
135
 
        logentry = lf.logs[0]
136
 
        eq(logentry.revno, '2')
137
 
        eq(logentry.rev.message, 'add one file')
138
 
        d = logentry.delta
139
 
        self.log('log 2 delta: %r' % d)
140
 
        # self.checkDelta(d, added=['hello'])
141
 
        
142
 
        # commit a log message with control characters
143
 
        msg = "All 8-bit chars: " +  ''.join([unichr(x) for x in range(256)])
144
 
        self.log("original commit message: %r", msg)
 
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')
145
143
        wt.commit(msg)
146
144
        lf = LogCatcher()
147
 
        show_log(b, lf, verbose=True)
 
145
        log.show_log(wt.branch, lf, verbose=True)
148
146
        committed_msg = lf.logs[0].rev.message
149
 
        self.log("escaped commit message: %r", committed_msg)
150
 
        self.assert_(msg != committed_msg)
151
 
        self.assert_(len(committed_msg) > len(msg))
 
147
        self.assertNotEqual(msg, committed_msg)
 
148
        self.assertTrue(len(committed_msg) > len(msg))
152
149
 
153
 
        # Check that log message with only XML-valid characters isn't
 
150
    def test_commit_message_without_control_chars(self):
 
151
        wt = self.make_branch_and_tree('.')
154
152
        # escaped.  As ElementTree apparently does some kind of
155
153
        # newline conversion, neither LF (\x0A) nor CR (\x0D) are
156
154
        # included in the test commit message, even though they are
157
155
        # valid XML 1.0 characters.
158
156
        msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
159
 
        self.log("original commit message: %r", msg)
160
157
        wt.commit(msg)
161
158
        lf = LogCatcher()
162
 
        show_log(b, lf, verbose=True)
 
159
        log.show_log(wt.branch, lf, verbose=True)
163
160
        committed_msg = lf.logs[0].rev.message
164
 
        self.log("escaped commit message: %r", committed_msg)
165
 
        self.assert_(msg == committed_msg)
 
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
 
 
202
def make_commits_with_trailing_newlines(wt):
 
203
    """Helper method for LogFormatter tests"""
 
204
    b = wt.branch
 
205
    b.nick='test'
 
206
    open('a', 'wb').write('hello moto\n')
 
207
    wt.add('a')
 
208
    wt.commit('simple log message', rev_id='a1',
 
209
              timestamp=1132586655.459960938, timezone=-6*3600,
 
210
              committer='Joe Foo <joe@foo.com>')
 
211
    open('b', 'wb').write('goodbye\n')
 
212
    wt.add('b')
 
213
    wt.commit('multiline\nlog\nmessage\n', rev_id='a2',
 
214
              timestamp=1132586842.411175966, timezone=-6*3600,
 
215
              committer='Joe Foo <joe@foo.com>',
 
216
              authors=['Joe Bar <joe@bar.com>'])
 
217
 
 
218
    open('c', 'wb').write('just another manic monday\n')
 
219
    wt.add('c')
 
220
    wt.commit('single line with trailing newline\n', rev_id='a3',
 
221
              timestamp=1132587176.835228920, timezone=-6*3600,
 
222
              committer = 'Joe Foo <joe@foo.com>')
 
223
    return b
 
224
 
 
225
 
 
226
def normalize_log(log):
 
227
    """Replaces the variable lines of logs with fixed lines"""
 
228
    author = 'author: Dolor Sit <test@example.com>'
 
229
    committer = 'committer: Lorem Ipsum <test@example.com>'
 
230
    lines = log.splitlines(True)
 
231
    for idx,line in enumerate(lines):
 
232
        stripped_line = line.lstrip()
 
233
        indent = ' ' * (len(line) - len(stripped_line))
 
234
        if stripped_line.startswith('author:'):
 
235
            lines[idx] = indent + author + '\n'
 
236
        elif stripped_line.startswith('committer:'):
 
237
            lines[idx] = indent + committer + '\n'
 
238
        elif stripped_line.startswith('timestamp:'):
 
239
            lines[idx] = indent + 'timestamp: Just now\n'
 
240
    return ''.join(lines)
 
241
 
 
242
 
 
243
class TestShortLogFormatter(tests.TestCaseWithTransport):
166
244
 
167
245
    def test_trailing_newlines(self):
168
246
        wt = self.make_branch_and_tree('.')
169
 
        b = wt.branch
170
 
        b.nick='test'
171
 
        open('a', 'wb').write('hello moto\n')
172
 
        wt.add('a')
173
 
        wt.commit('simple log message', rev_id='a1'
174
 
                , timestamp=1132586655.459960938, timezone=-6*3600
175
 
                , committer='Joe Foo <joe@foo.com>')
176
 
        open('b', 'wb').write('goodbye\n')
177
 
        wt.add('b')
178
 
        wt.commit('multiline\nlog\nmessage\n', rev_id='a2'
179
 
                , timestamp=1132586842.411175966, timezone=-6*3600
180
 
                , committer='Joe Foo <joe@foo.com>')
181
 
 
182
 
        open('c', 'wb').write('just another manic monday\n')
183
 
        wt.add('c')
184
 
        wt.commit('single line with trailing newline\n', rev_id='a3'
185
 
                , timestamp=1132587176.835228920, timezone=-6*3600
186
 
                , committer = 'Joe Foo <joe@foo.com>')
187
 
 
188
 
        sio = StringIO()
189
 
        lf = ShortLogFormatter(to_file=sio)
190
 
        show_log(b, lf)
191
 
        self.assertEquals(sio.getvalue(), """\
 
247
        b = make_commits_with_trailing_newlines(wt)
 
248
        sio = self.make_utf8_encoded_stringio()
 
249
        lf = log.ShortLogFormatter(to_file=sio)
 
250
        log.show_log(b, lf)
 
251
        self.assertEqualDiff("""\
192
252
    3 Joe Foo\t2005-11-21
193
253
      single line with trailing newline
194
254
 
195
 
    2 Joe Foo\t2005-11-21
 
255
    2 Joe Bar\t2005-11-21
196
256
      multiline
197
257
      log
198
258
      message
200
260
    1 Joe Foo\t2005-11-21
201
261
      simple log message
202
262
 
203
 
""")
204
 
 
205
 
        sio = StringIO()
206
 
        lf = LongLogFormatter(to_file=sio)
207
 
        show_log(b, lf)
208
 
        self.assertEquals(sio.getvalue(), """\
209
 
------------------------------------------------------------
210
 
revno: 3
211
 
committer: Joe Foo <joe@foo.com>
212
 
branch nick: test
213
 
timestamp: Mon 2005-11-21 09:32:56 -0600
214
 
message:
215
 
  single line with trailing newline
216
 
------------------------------------------------------------
217
 
revno: 2
218
 
committer: Joe Foo <joe@foo.com>
219
 
branch nick: test
220
 
timestamp: Mon 2005-11-21 09:27:22 -0600
221
 
message:
222
 
  multiline
223
 
  log
224
 
  message
225
 
------------------------------------------------------------
226
 
revno: 1
227
 
committer: Joe Foo <joe@foo.com>
228
 
branch nick: test
229
 
timestamp: Mon 2005-11-21 09:24:15 -0600
230
 
message:
231
 
  simple log message
232
 
""")
233
 
        
 
263
""",
 
264
                             sio.getvalue())
 
265
 
 
266
    def _prepare_tree_with_merges(self, with_tags=False):
 
267
        wt = self.make_branch_and_memory_tree('.')
 
268
        wt.lock_write()
 
269
        self.addCleanup(wt.unlock)
 
270
        wt.add('')
 
271
        wt.commit('rev-1', rev_id='rev-1',
 
272
                  timestamp=1132586655, timezone=36000,
 
273
                  committer='Joe Foo <joe@foo.com>')
 
274
        wt.commit('rev-merged', rev_id='rev-2a',
 
275
                  timestamp=1132586700, timezone=36000,
 
276
                  committer='Joe Foo <joe@foo.com>')
 
277
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
278
        wt.branch.set_last_revision_info(1, 'rev-1')
 
279
        wt.commit('rev-2', rev_id='rev-2b',
 
280
                  timestamp=1132586800, timezone=36000,
 
281
                  committer='Joe Foo <joe@foo.com>')
 
282
        if with_tags:
 
283
            branch = wt.branch
 
284
            branch.tags.set_tag('v0.2', 'rev-2b')
 
285
            wt.commit('rev-3', rev_id='rev-3',
 
286
                      timestamp=1132586900, timezone=36000,
 
287
                      committer='Jane Foo <jane@foo.com>')
 
288
            branch.tags.set_tag('v1.0rc1', 'rev-3')
 
289
            branch.tags.set_tag('v1.0', 'rev-3')
 
290
        return wt
 
291
 
 
292
    def test_short_log_with_merges(self):
 
293
        wt = self._prepare_tree_with_merges()
 
294
        logfile = self.make_utf8_encoded_stringio()
 
295
        formatter = log.ShortLogFormatter(to_file=logfile)
 
296
        log.show_log(wt.branch, formatter)
 
297
        self.assertEqualDiff("""\
 
298
    2 Joe Foo\t2005-11-22 [merge]
 
299
      rev-2
 
300
 
 
301
    1 Joe Foo\t2005-11-22
 
302
      rev-1
 
303
 
 
304
Use --levels 0 (or -n0) to see merged revisions.
 
305
""",
 
306
                             logfile.getvalue())
 
307
 
 
308
    def test_short_log_with_merges_and_range(self):
 
309
        wt = self.make_branch_and_memory_tree('.')
 
310
        wt.lock_write()
 
311
        self.addCleanup(wt.unlock)
 
312
        wt.add('')
 
313
        wt.commit('rev-1', rev_id='rev-1',
 
314
                  timestamp=1132586655, timezone=36000,
 
315
                  committer='Joe Foo <joe@foo.com>')
 
316
        wt.commit('rev-merged', rev_id='rev-2a',
 
317
                  timestamp=1132586700, timezone=36000,
 
318
                  committer='Joe Foo <joe@foo.com>')
 
319
        wt.branch.set_last_revision_info(1, 'rev-1')
 
320
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
321
        wt.commit('rev-2b', rev_id='rev-2b',
 
322
                  timestamp=1132586800, timezone=36000,
 
323
                  committer='Joe Foo <joe@foo.com>')
 
324
        wt.commit('rev-3a', rev_id='rev-3a',
 
325
                  timestamp=1132586800, timezone=36000,
 
326
                  committer='Joe Foo <joe@foo.com>')
 
327
        wt.branch.set_last_revision_info(2, 'rev-2b')
 
328
        wt.set_parent_ids(['rev-2b', 'rev-3a'])
 
329
        wt.commit('rev-3b', rev_id='rev-3b',
 
330
                  timestamp=1132586800, timezone=36000,
 
331
                  committer='Joe Foo <joe@foo.com>')
 
332
        logfile = self.make_utf8_encoded_stringio()
 
333
        formatter = log.ShortLogFormatter(to_file=logfile)
 
334
        log.show_log(wt.branch, formatter,
 
335
            start_revision=2, end_revision=3)
 
336
        self.assertEqualDiff("""\
 
337
    3 Joe Foo\t2005-11-22 [merge]
 
338
      rev-3b
 
339
 
 
340
    2 Joe Foo\t2005-11-22 [merge]
 
341
      rev-2b
 
342
 
 
343
Use --levels 0 (or -n0) to see merged revisions.
 
344
""",
 
345
                             logfile.getvalue())
 
346
 
 
347
    def test_short_log_with_tags(self):
 
348
        wt = self._prepare_tree_with_merges(with_tags=True)
 
349
        logfile = self.make_utf8_encoded_stringio()
 
350
        formatter = log.ShortLogFormatter(to_file=logfile)
 
351
        log.show_log(wt.branch, formatter)
 
352
        self.assertEqualDiff("""\
 
353
    3 Jane Foo\t2005-11-22 {v1.0, v1.0rc1}
 
354
      rev-3
 
355
 
 
356
    2 Joe Foo\t2005-11-22 {v0.2} [merge]
 
357
      rev-2
 
358
 
 
359
    1 Joe Foo\t2005-11-22
 
360
      rev-1
 
361
 
 
362
Use --levels 0 (or -n0) to see merged revisions.
 
363
""",
 
364
                             logfile.getvalue())
 
365
 
 
366
    def test_short_log_single_merge_revision(self):
 
367
        wt = self.make_branch_and_memory_tree('.')
 
368
        wt.lock_write()
 
369
        self.addCleanup(wt.unlock)
 
370
        wt.add('')
 
371
        wt.commit('rev-1', rev_id='rev-1',
 
372
                  timestamp=1132586655, timezone=36000,
 
373
                  committer='Joe Foo <joe@foo.com>')
 
374
        wt.commit('rev-merged', rev_id='rev-2a',
 
375
                  timestamp=1132586700, timezone=36000,
 
376
                  committer='Joe Foo <joe@foo.com>')
 
377
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
378
        wt.branch.set_last_revision_info(1, 'rev-1')
 
379
        wt.commit('rev-2', rev_id='rev-2b',
 
380
                  timestamp=1132586800, timezone=36000,
 
381
                  committer='Joe Foo <joe@foo.com>')
 
382
        logfile = self.make_utf8_encoded_stringio()
 
383
        formatter = log.ShortLogFormatter(to_file=logfile)
 
384
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
 
385
        wtb = wt.branch
 
386
        rev = revspec.in_history(wtb)
 
387
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
388
        self.assertEqualDiff("""\
 
389
      1.1.1 Joe Foo\t2005-11-22
 
390
            rev-merged
 
391
 
 
392
""",
 
393
                             logfile.getvalue())
 
394
 
 
395
 
 
396
class TestShortLogFormatterWithMergeRevisions(tests.TestCaseWithTransport):
 
397
 
 
398
    def test_short_merge_revs_log_with_merges(self):
 
399
        wt = self.make_branch_and_memory_tree('.')
 
400
        wt.lock_write()
 
401
        self.addCleanup(wt.unlock)
 
402
        wt.add('')
 
403
        wt.commit('rev-1', rev_id='rev-1',
 
404
                  timestamp=1132586655, timezone=36000,
 
405
                  committer='Joe Foo <joe@foo.com>')
 
406
        wt.commit('rev-merged', rev_id='rev-2a',
 
407
                  timestamp=1132586700, timezone=36000,
 
408
                  committer='Joe Foo <joe@foo.com>')
 
409
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
410
        wt.branch.set_last_revision_info(1, 'rev-1')
 
411
        wt.commit('rev-2', rev_id='rev-2b',
 
412
                  timestamp=1132586800, timezone=36000,
 
413
                  committer='Joe Foo <joe@foo.com>')
 
414
        logfile = self.make_utf8_encoded_stringio()
 
415
        formatter = log.ShortLogFormatter(to_file=logfile, levels=0)
 
416
        log.show_log(wt.branch, formatter)
 
417
        # Note that the 1.1.1 indenting is in fact correct given that
 
418
        # the revision numbers are right justified within 5 characters
 
419
        # for mainline revnos and 9 characters for dotted revnos.
 
420
        self.assertEqualDiff("""\
 
421
    2 Joe Foo\t2005-11-22 [merge]
 
422
      rev-2
 
423
 
 
424
          1.1.1 Joe Foo\t2005-11-22
 
425
                rev-merged
 
426
 
 
427
    1 Joe Foo\t2005-11-22
 
428
      rev-1
 
429
 
 
430
""",
 
431
                             logfile.getvalue())
 
432
 
 
433
    def test_short_merge_revs_log_single_merge_revision(self):
 
434
        wt = self.make_branch_and_memory_tree('.')
 
435
        wt.lock_write()
 
436
        self.addCleanup(wt.unlock)
 
437
        wt.add('')
 
438
        wt.commit('rev-1', rev_id='rev-1',
 
439
                  timestamp=1132586655, timezone=36000,
 
440
                  committer='Joe Foo <joe@foo.com>')
 
441
        wt.commit('rev-merged', rev_id='rev-2a',
 
442
                  timestamp=1132586700, timezone=36000,
 
443
                  committer='Joe Foo <joe@foo.com>')
 
444
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
445
        wt.branch.set_last_revision_info(1, 'rev-1')
 
446
        wt.commit('rev-2', rev_id='rev-2b',
 
447
                  timestamp=1132586800, timezone=36000,
 
448
                  committer='Joe Foo <joe@foo.com>')
 
449
        logfile = self.make_utf8_encoded_stringio()
 
450
        formatter = log.ShortLogFormatter(to_file=logfile, levels=0)
 
451
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
 
452
        wtb = wt.branch
 
453
        rev = revspec.in_history(wtb)
 
454
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
455
        self.assertEqualDiff("""\
 
456
      1.1.1 Joe Foo\t2005-11-22
 
457
            rev-merged
 
458
 
 
459
""",
 
460
                             logfile.getvalue())
 
461
 
 
462
 
 
463
class TestLongLogFormatter(TestCaseWithoutPropsHandler):
 
464
 
234
465
    def test_verbose_log(self):
235
466
        """Verbose log includes changed files
236
 
        
237
 
        bug #4676
238
 
        """
239
 
        wt = self.make_branch_and_tree('.')
240
 
        b = wt.branch
241
 
        self.build_tree(['a'])
242
 
        wt.add('a')
243
 
        # XXX: why does a longer nick show up?
244
 
        b.nick = 'test_verbose_log'
245
 
        wt.commit(message='add a', 
246
 
                  timestamp=1132711707, 
247
 
                  timezone=36000,
248
 
                  committer='Lorem Ipsum <test@example.com>')
249
 
        logfile = file('out.tmp', 'w+')
250
 
        formatter = LongLogFormatter(to_file=logfile)
251
 
        show_log(b, formatter, verbose=True)
252
 
        logfile.flush()
253
 
        logfile.seek(0)
254
 
        log_contents = logfile.read()
255
 
        self.assertEqualDiff(log_contents, '''\
256
 
------------------------------------------------------------
257
 
revno: 1
258
 
committer: Lorem Ipsum <test@example.com>
259
 
branch nick: test_verbose_log
260
 
timestamp: Wed 2005-11-23 12:08:27 +1000
261
 
message:
262
 
  add a
263
 
added:
264
 
  a
265
 
''')
 
467
 
 
468
        bug #4676
 
469
        """
 
470
        wt = self.make_branch_and_tree('.')
 
471
        b = wt.branch
 
472
        self.build_tree(['a'])
 
473
        wt.add('a')
 
474
        # XXX: why does a longer nick show up?
 
475
        b.nick = 'test_verbose_log'
 
476
        wt.commit(message='add a',
 
477
                  timestamp=1132711707,
 
478
                  timezone=36000,
 
479
                  committer='Lorem Ipsum <test@example.com>')
 
480
        logfile = file('out.tmp', 'w+')
 
481
        formatter = log.LongLogFormatter(to_file=logfile)
 
482
        log.show_log(b, formatter, verbose=True)
 
483
        logfile.flush()
 
484
        logfile.seek(0)
 
485
        log_contents = logfile.read()
 
486
        self.assertEqualDiff('''\
 
487
------------------------------------------------------------
 
488
revno: 1
 
489
committer: Lorem Ipsum <test@example.com>
 
490
branch nick: test_verbose_log
 
491
timestamp: Wed 2005-11-23 12:08:27 +1000
 
492
message:
 
493
  add a
 
494
added:
 
495
  a
 
496
''',
 
497
                             log_contents)
 
498
 
 
499
    def test_merges_are_indented_by_level(self):
 
500
        wt = self.make_branch_and_tree('parent')
 
501
        wt.commit('first post')
 
502
        self.run_bzr('branch parent child')
 
503
        self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
 
504
        self.run_bzr('branch child smallerchild')
 
505
        self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
 
506
            'smallerchild'])
 
507
        os.chdir('child')
 
508
        self.run_bzr('merge ../smallerchild')
 
509
        self.run_bzr(['commit', '-m', 'merge branch 2'])
 
510
        os.chdir('../parent')
 
511
        self.run_bzr('merge ../child')
 
512
        wt.commit('merge branch 1')
 
513
        b = wt.branch
 
514
        sio = self.make_utf8_encoded_stringio()
 
515
        lf = log.LongLogFormatter(to_file=sio, levels=0)
 
516
        log.show_log(b, lf, verbose=True)
 
517
        the_log = normalize_log(sio.getvalue())
 
518
        self.assertEqualDiff("""\
 
519
------------------------------------------------------------
 
520
revno: 2 [merge]
 
521
committer: Lorem Ipsum <test@example.com>
 
522
branch nick: parent
 
523
timestamp: Just now
 
524
message:
 
525
  merge branch 1
 
526
    ------------------------------------------------------------
 
527
    revno: 1.1.2 [merge]
 
528
    committer: Lorem Ipsum <test@example.com>
 
529
    branch nick: child
 
530
    timestamp: Just now
 
531
    message:
 
532
      merge branch 2
 
533
        ------------------------------------------------------------
 
534
        revno: 1.2.1
 
535
        committer: Lorem Ipsum <test@example.com>
 
536
        branch nick: smallerchild
 
537
        timestamp: Just now
 
538
        message:
 
539
          branch 2
 
540
    ------------------------------------------------------------
 
541
    revno: 1.1.1
 
542
    committer: Lorem Ipsum <test@example.com>
 
543
    branch nick: child
 
544
    timestamp: Just now
 
545
    message:
 
546
      branch 1
 
547
------------------------------------------------------------
 
548
revno: 1
 
549
committer: Lorem Ipsum <test@example.com>
 
550
branch nick: parent
 
551
timestamp: Just now
 
552
message:
 
553
  first post
 
554
""",
 
555
                             the_log)
 
556
 
 
557
    def test_verbose_merge_revisions_contain_deltas(self):
 
558
        wt = self.make_branch_and_tree('parent')
 
559
        self.build_tree(['parent/f1', 'parent/f2'])
 
560
        wt.add(['f1','f2'])
 
561
        wt.commit('first post')
 
562
        self.run_bzr('branch parent child')
 
563
        os.unlink('child/f1')
 
564
        file('child/f2', 'wb').write('hello\n')
 
565
        self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
 
566
            'child'])
 
567
        os.chdir('parent')
 
568
        self.run_bzr('merge ../child')
 
569
        wt.commit('merge branch 1')
 
570
        b = wt.branch
 
571
        sio = self.make_utf8_encoded_stringio()
 
572
        lf = log.LongLogFormatter(to_file=sio, levels=0)
 
573
        log.show_log(b, lf, verbose=True)
 
574
        the_log = normalize_log(sio.getvalue())
 
575
        self.assertEqualDiff("""\
 
576
------------------------------------------------------------
 
577
revno: 2 [merge]
 
578
committer: Lorem Ipsum <test@example.com>
 
579
branch nick: parent
 
580
timestamp: Just now
 
581
message:
 
582
  merge branch 1
 
583
removed:
 
584
  f1
 
585
modified:
 
586
  f2
 
587
    ------------------------------------------------------------
 
588
    revno: 1.1.1
 
589
    committer: Lorem Ipsum <test@example.com>
 
590
    branch nick: child
 
591
    timestamp: Just now
 
592
    message:
 
593
      removed f1 and modified f2
 
594
    removed:
 
595
      f1
 
596
    modified:
 
597
      f2
 
598
------------------------------------------------------------
 
599
revno: 1
 
600
committer: Lorem Ipsum <test@example.com>
 
601
branch nick: parent
 
602
timestamp: Just now
 
603
message:
 
604
  first post
 
605
added:
 
606
  f1
 
607
  f2
 
608
""",
 
609
                             the_log)
 
610
 
 
611
    def test_trailing_newlines(self):
 
612
        wt = self.make_branch_and_tree('.')
 
613
        b = make_commits_with_trailing_newlines(wt)
 
614
        sio = self.make_utf8_encoded_stringio()
 
615
        lf = log.LongLogFormatter(to_file=sio)
 
616
        log.show_log(b, lf)
 
617
        self.assertEqualDiff("""\
 
618
------------------------------------------------------------
 
619
revno: 3
 
620
committer: Joe Foo <joe@foo.com>
 
621
branch nick: test
 
622
timestamp: Mon 2005-11-21 09:32:56 -0600
 
623
message:
 
624
  single line with trailing newline
 
625
------------------------------------------------------------
 
626
revno: 2
 
627
author: Joe Bar <joe@bar.com>
 
628
committer: Joe Foo <joe@foo.com>
 
629
branch nick: test
 
630
timestamp: Mon 2005-11-21 09:27:22 -0600
 
631
message:
 
632
  multiline
 
633
  log
 
634
  message
 
635
------------------------------------------------------------
 
636
revno: 1
 
637
committer: Joe Foo <joe@foo.com>
 
638
branch nick: test
 
639
timestamp: Mon 2005-11-21 09:24:15 -0600
 
640
message:
 
641
  simple log message
 
642
""",
 
643
                             sio.getvalue())
 
644
 
 
645
    def test_author_in_log(self):
 
646
        """Log includes the author name if it's set in
 
647
        the revision properties
 
648
        """
 
649
        wt = self.make_branch_and_tree('.')
 
650
        b = wt.branch
 
651
        self.build_tree(['a'])
 
652
        wt.add('a')
 
653
        b.nick = 'test_author_log'
 
654
        wt.commit(message='add a',
 
655
                  timestamp=1132711707,
 
656
                  timezone=36000,
 
657
                  committer='Lorem Ipsum <test@example.com>',
 
658
                  authors=['John Doe <jdoe@example.com>',
 
659
                           'Jane Rey <jrey@example.com>'])
 
660
        sio = StringIO()
 
661
        formatter = log.LongLogFormatter(to_file=sio)
 
662
        log.show_log(b, formatter)
 
663
        self.assertEqualDiff('''\
 
664
------------------------------------------------------------
 
665
revno: 1
 
666
author: John Doe <jdoe@example.com>, Jane Rey <jrey@example.com>
 
667
committer: Lorem Ipsum <test@example.com>
 
668
branch nick: test_author_log
 
669
timestamp: Wed 2005-11-23 12:08:27 +1000
 
670
message:
 
671
  add a
 
672
''',
 
673
                             sio.getvalue())
 
674
 
 
675
    def test_properties_in_log(self):
 
676
        """Log includes the custom properties returned by the registered
 
677
        handlers.
 
678
        """
 
679
        wt = self.make_branch_and_tree('.')
 
680
        b = wt.branch
 
681
        self.build_tree(['a'])
 
682
        wt.add('a')
 
683
        b.nick = 'test_properties_in_log'
 
684
        wt.commit(message='add a',
 
685
                  timestamp=1132711707,
 
686
                  timezone=36000,
 
687
                  committer='Lorem Ipsum <test@example.com>',
 
688
                  authors=['John Doe <jdoe@example.com>'])
 
689
        sio = StringIO()
 
690
        formatter = log.LongLogFormatter(to_file=sio)
 
691
        try:
 
692
            def trivial_custom_prop_handler(revision):
 
693
                return {'test_prop':'test_value'}
 
694
 
 
695
            log.properties_handler_registry.register(
 
696
                'trivial_custom_prop_handler',
 
697
                trivial_custom_prop_handler)
 
698
            log.show_log(b, formatter)
 
699
        finally:
 
700
            log.properties_handler_registry.remove(
 
701
                'trivial_custom_prop_handler')
 
702
            self.assertEqualDiff('''\
 
703
------------------------------------------------------------
 
704
revno: 1
 
705
test_prop: test_value
 
706
author: John Doe <jdoe@example.com>
 
707
committer: Lorem Ipsum <test@example.com>
 
708
branch nick: test_properties_in_log
 
709
timestamp: Wed 2005-11-23 12:08:27 +1000
 
710
message:
 
711
  add a
 
712
''',
 
713
                                 sio.getvalue())
 
714
 
 
715
    def test_properties_in_short_log(self):
 
716
        """Log includes the custom properties returned by the registered
 
717
        handlers.
 
718
        """
 
719
        wt = self.make_branch_and_tree('.')
 
720
        b = wt.branch
 
721
        self.build_tree(['a'])
 
722
        wt.add('a')
 
723
        b.nick = 'test_properties_in_short_log'
 
724
        wt.commit(message='add a',
 
725
                  timestamp=1132711707,
 
726
                  timezone=36000,
 
727
                  committer='Lorem Ipsum <test@example.com>',
 
728
                  authors=['John Doe <jdoe@example.com>'])
 
729
        sio = StringIO()
 
730
        formatter = log.ShortLogFormatter(to_file=sio)
 
731
        try:
 
732
            def trivial_custom_prop_handler(revision):
 
733
                return {'test_prop':'test_value'}
 
734
 
 
735
            log.properties_handler_registry.register(
 
736
                'trivial_custom_prop_handler',
 
737
                trivial_custom_prop_handler)
 
738
            log.show_log(b, formatter)
 
739
        finally:
 
740
            log.properties_handler_registry.remove(
 
741
                'trivial_custom_prop_handler')
 
742
            self.assertEqualDiff('''\
 
743
    1 John Doe\t2005-11-23
 
744
      test_prop: test_value
 
745
      add a
 
746
 
 
747
''',
 
748
                                 sio.getvalue())
 
749
 
 
750
    def test_error_in_properties_handler(self):
 
751
        """Log includes the custom properties returned by the registered
 
752
        handlers.
 
753
        """
 
754
        wt = self.make_branch_and_tree('.')
 
755
        b = wt.branch
 
756
        self.build_tree(['a'])
 
757
        wt.add('a')
 
758
        b.nick = 'test_author_log'
 
759
        wt.commit(message='add a',
 
760
                  timestamp=1132711707,
 
761
                  timezone=36000,
 
762
                  committer='Lorem Ipsum <test@example.com>',
 
763
                  authors=['John Doe <jdoe@example.com>'],
 
764
                  revprops={'first_prop':'first_value'})
 
765
        sio = StringIO()
 
766
        formatter = log.LongLogFormatter(to_file=sio)
 
767
        try:
 
768
            def trivial_custom_prop_handler(revision):
 
769
                raise StandardError("a test error")
 
770
 
 
771
            log.properties_handler_registry.register(
 
772
                'trivial_custom_prop_handler',
 
773
                trivial_custom_prop_handler)
 
774
            self.assertRaises(StandardError, log.show_log, b, formatter,)
 
775
        finally:
 
776
            log.properties_handler_registry.remove(
 
777
                'trivial_custom_prop_handler')
 
778
 
 
779
    def test_properties_handler_bad_argument(self):
 
780
        wt = self.make_branch_and_tree('.')
 
781
        b = wt.branch
 
782
        self.build_tree(['a'])
 
783
        wt.add('a')
 
784
        b.nick = 'test_author_log'
 
785
        wt.commit(message='add a',
 
786
                  timestamp=1132711707,
 
787
                  timezone=36000,
 
788
                  committer='Lorem Ipsum <test@example.com>',
 
789
                  authors=['John Doe <jdoe@example.com>'],
 
790
                  revprops={'a_prop':'test_value'})
 
791
        sio = StringIO()
 
792
        formatter = log.LongLogFormatter(to_file=sio)
 
793
        try:
 
794
            def bad_argument_prop_handler(revision):
 
795
                return {'custom_prop_name':revision.properties['a_prop']}
 
796
 
 
797
            log.properties_handler_registry.register(
 
798
                'bad_argument_prop_handler',
 
799
                bad_argument_prop_handler)
 
800
 
 
801
            self.assertRaises(AttributeError, formatter.show_properties,
 
802
                              'a revision', '')
 
803
 
 
804
            revision = b.repository.get_revision(b.last_revision())
 
805
            formatter.show_properties(revision, '')
 
806
            self.assertEqualDiff('''custom_prop_name: test_value\n''',
 
807
                                 sio.getvalue())
 
808
        finally:
 
809
            log.properties_handler_registry.remove(
 
810
                'bad_argument_prop_handler')
 
811
 
 
812
 
 
813
class TestLongLogFormatterWithoutMergeRevisions(TestCaseWithoutPropsHandler):
 
814
 
 
815
    def test_long_verbose_log(self):
 
816
        """Verbose log includes changed files
 
817
 
 
818
        bug #4676
 
819
        """
 
820
        wt = self.make_branch_and_tree('.')
 
821
        b = wt.branch
 
822
        self.build_tree(['a'])
 
823
        wt.add('a')
 
824
        # XXX: why does a longer nick show up?
 
825
        b.nick = 'test_verbose_log'
 
826
        wt.commit(message='add a',
 
827
                  timestamp=1132711707,
 
828
                  timezone=36000,
 
829
                  committer='Lorem Ipsum <test@example.com>')
 
830
        logfile = file('out.tmp', 'w+')
 
831
        formatter = log.LongLogFormatter(to_file=logfile, levels=1)
 
832
        log.show_log(b, formatter, verbose=True)
 
833
        logfile.flush()
 
834
        logfile.seek(0)
 
835
        log_contents = logfile.read()
 
836
        self.assertEqualDiff('''\
 
837
------------------------------------------------------------
 
838
revno: 1
 
839
committer: Lorem Ipsum <test@example.com>
 
840
branch nick: test_verbose_log
 
841
timestamp: Wed 2005-11-23 12:08:27 +1000
 
842
message:
 
843
  add a
 
844
added:
 
845
  a
 
846
''',
 
847
                             log_contents)
 
848
 
 
849
    def test_long_verbose_contain_deltas(self):
 
850
        wt = self.make_branch_and_tree('parent')
 
851
        self.build_tree(['parent/f1', 'parent/f2'])
 
852
        wt.add(['f1','f2'])
 
853
        wt.commit('first post')
 
854
        self.run_bzr('branch parent child')
 
855
        os.unlink('child/f1')
 
856
        file('child/f2', 'wb').write('hello\n')
 
857
        self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
 
858
            'child'])
 
859
        os.chdir('parent')
 
860
        self.run_bzr('merge ../child')
 
861
        wt.commit('merge branch 1')
 
862
        b = wt.branch
 
863
        sio = self.make_utf8_encoded_stringio()
 
864
        lf = log.LongLogFormatter(to_file=sio, levels=1)
 
865
        log.show_log(b, lf, verbose=True)
 
866
        the_log = normalize_log(sio.getvalue())
 
867
        self.assertEqualDiff("""\
 
868
------------------------------------------------------------
 
869
revno: 2 [merge]
 
870
committer: Lorem Ipsum <test@example.com>
 
871
branch nick: parent
 
872
timestamp: Just now
 
873
message:
 
874
  merge branch 1
 
875
removed:
 
876
  f1
 
877
modified:
 
878
  f2
 
879
------------------------------------------------------------
 
880
revno: 1
 
881
committer: Lorem Ipsum <test@example.com>
 
882
branch nick: parent
 
883
timestamp: Just now
 
884
message:
 
885
  first post
 
886
added:
 
887
  f1
 
888
  f2
 
889
------------------------------------------------------------
 
890
Use --levels 0 (or -n0) to see merged revisions.
 
891
""",
 
892
                             the_log)
 
893
 
 
894
    def test_long_trailing_newlines(self):
 
895
        wt = self.make_branch_and_tree('.')
 
896
        b = make_commits_with_trailing_newlines(wt)
 
897
        sio = self.make_utf8_encoded_stringio()
 
898
        lf = log.LongLogFormatter(to_file=sio, levels=1)
 
899
        log.show_log(b, lf)
 
900
        self.assertEqualDiff("""\
 
901
------------------------------------------------------------
 
902
revno: 3
 
903
committer: Joe Foo <joe@foo.com>
 
904
branch nick: test
 
905
timestamp: Mon 2005-11-21 09:32:56 -0600
 
906
message:
 
907
  single line with trailing newline
 
908
------------------------------------------------------------
 
909
revno: 2
 
910
author: Joe Bar <joe@bar.com>
 
911
committer: Joe Foo <joe@foo.com>
 
912
branch nick: test
 
913
timestamp: Mon 2005-11-21 09:27:22 -0600
 
914
message:
 
915
  multiline
 
916
  log
 
917
  message
 
918
------------------------------------------------------------
 
919
revno: 1
 
920
committer: Joe Foo <joe@foo.com>
 
921
branch nick: test
 
922
timestamp: Mon 2005-11-21 09:24:15 -0600
 
923
message:
 
924
  simple log message
 
925
""",
 
926
                             sio.getvalue())
 
927
 
 
928
    def test_long_author_in_log(self):
 
929
        """Log includes the author name if it's set in
 
930
        the revision properties
 
931
        """
 
932
        wt = self.make_branch_and_tree('.')
 
933
        b = wt.branch
 
934
        self.build_tree(['a'])
 
935
        wt.add('a')
 
936
        b.nick = 'test_author_log'
 
937
        wt.commit(message='add a',
 
938
                  timestamp=1132711707,
 
939
                  timezone=36000,
 
940
                  committer='Lorem Ipsum <test@example.com>',
 
941
                  authors=['John Doe <jdoe@example.com>'])
 
942
        sio = StringIO()
 
943
        formatter = log.LongLogFormatter(to_file=sio, levels=1)
 
944
        log.show_log(b, formatter)
 
945
        self.assertEqualDiff('''\
 
946
------------------------------------------------------------
 
947
revno: 1
 
948
author: John Doe <jdoe@example.com>
 
949
committer: Lorem Ipsum <test@example.com>
 
950
branch nick: test_author_log
 
951
timestamp: Wed 2005-11-23 12:08:27 +1000
 
952
message:
 
953
  add a
 
954
''',
 
955
                             sio.getvalue())
 
956
 
 
957
    def test_long_properties_in_log(self):
 
958
        """Log includes the custom properties returned by the registered
 
959
        handlers.
 
960
        """
 
961
        wt = self.make_branch_and_tree('.')
 
962
        b = wt.branch
 
963
        self.build_tree(['a'])
 
964
        wt.add('a')
 
965
        b.nick = 'test_properties_in_log'
 
966
        wt.commit(message='add a',
 
967
                  timestamp=1132711707,
 
968
                  timezone=36000,
 
969
                  committer='Lorem Ipsum <test@example.com>',
 
970
                  authors=['John Doe <jdoe@example.com>'])
 
971
        sio = StringIO()
 
972
        formatter = log.LongLogFormatter(to_file=sio, levels=1)
 
973
        try:
 
974
            def trivial_custom_prop_handler(revision):
 
975
                return {'test_prop':'test_value'}
 
976
 
 
977
            log.properties_handler_registry.register(
 
978
                'trivial_custom_prop_handler',
 
979
                trivial_custom_prop_handler)
 
980
            log.show_log(b, formatter)
 
981
        finally:
 
982
            log.properties_handler_registry.remove(
 
983
                'trivial_custom_prop_handler')
 
984
            self.assertEqualDiff('''\
 
985
------------------------------------------------------------
 
986
revno: 1
 
987
test_prop: test_value
 
988
author: John Doe <jdoe@example.com>
 
989
committer: Lorem Ipsum <test@example.com>
 
990
branch nick: test_properties_in_log
 
991
timestamp: Wed 2005-11-23 12:08:27 +1000
 
992
message:
 
993
  add a
 
994
''',
 
995
                                 sio.getvalue())
 
996
 
 
997
 
 
998
class TestLineLogFormatter(tests.TestCaseWithTransport):
266
999
 
267
1000
    def test_line_log(self):
268
1001
        """Line log should show revno
269
 
        
270
 
        bug #5162
271
 
        """
272
 
        wt = self.make_branch_and_tree('.')
273
 
        b = wt.branch
274
 
        self.build_tree(['a'])
275
 
        wt.add('a')
276
 
        b.nick = 'test-line-log'
277
 
        wt.commit(message='add a', 
278
 
                  timestamp=1132711707, 
279
 
                  timezone=36000,
280
 
                  committer='Line-Log-Formatter Tester <test@line.log>')
281
 
        logfile = file('out.tmp', 'w+')
282
 
        formatter = LineLogFormatter(to_file=logfile)
283
 
        show_log(b, formatter)
284
 
        logfile.flush()
285
 
        logfile.seek(0)
286
 
        log_contents = logfile.read()
287
 
        self.assertEqualDiff(log_contents, '1: Line-Log-Formatte... 2005-11-23 add a\n')
 
1002
 
 
1003
        bug #5162
 
1004
        """
 
1005
        wt = self.make_branch_and_tree('.')
 
1006
        b = wt.branch
 
1007
        self.build_tree(['a'])
 
1008
        wt.add('a')
 
1009
        b.nick = 'test-line-log'
 
1010
        wt.commit(message='add a',
 
1011
                  timestamp=1132711707,
 
1012
                  timezone=36000,
 
1013
                  committer='Line-Log-Formatter Tester <test@line.log>')
 
1014
        logfile = file('out.tmp', 'w+')
 
1015
        formatter = log.LineLogFormatter(to_file=logfile)
 
1016
        log.show_log(b, formatter)
 
1017
        logfile.flush()
 
1018
        logfile.seek(0)
 
1019
        log_contents = logfile.read()
 
1020
        self.assertEqualDiff('1: Line-Log-Formatte... 2005-11-23 add a\n',
 
1021
                             log_contents)
 
1022
 
 
1023
    def test_trailing_newlines(self):
 
1024
        wt = self.make_branch_and_tree('.')
 
1025
        b = make_commits_with_trailing_newlines(wt)
 
1026
        sio = self.make_utf8_encoded_stringio()
 
1027
        lf = log.LineLogFormatter(to_file=sio)
 
1028
        log.show_log(b, lf)
 
1029
        self.assertEqualDiff("""\
 
1030
3: Joe Foo 2005-11-21 single line with trailing newline
 
1031
2: Joe Bar 2005-11-21 multiline
 
1032
1: Joe Foo 2005-11-21 simple log message
 
1033
""",
 
1034
                             sio.getvalue())
 
1035
 
 
1036
    def _prepare_tree_with_merges(self, with_tags=False):
 
1037
        wt = self.make_branch_and_memory_tree('.')
 
1038
        wt.lock_write()
 
1039
        self.addCleanup(wt.unlock)
 
1040
        wt.add('')
 
1041
        wt.commit('rev-1', rev_id='rev-1',
 
1042
                  timestamp=1132586655, timezone=36000,
 
1043
                  committer='Joe Foo <joe@foo.com>')
 
1044
        wt.commit('rev-merged', rev_id='rev-2a',
 
1045
                  timestamp=1132586700, timezone=36000,
 
1046
                  committer='Joe Foo <joe@foo.com>')
 
1047
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
1048
        wt.branch.set_last_revision_info(1, 'rev-1')
 
1049
        wt.commit('rev-2', rev_id='rev-2b',
 
1050
                  timestamp=1132586800, timezone=36000,
 
1051
                  committer='Joe Foo <joe@foo.com>')
 
1052
        if with_tags:
 
1053
            branch = wt.branch
 
1054
            branch.tags.set_tag('v0.2', 'rev-2b')
 
1055
            wt.commit('rev-3', rev_id='rev-3',
 
1056
                      timestamp=1132586900, timezone=36000,
 
1057
                      committer='Jane Foo <jane@foo.com>')
 
1058
            branch.tags.set_tag('v1.0rc1', 'rev-3')
 
1059
            branch.tags.set_tag('v1.0', 'rev-3')
 
1060
        return wt
 
1061
 
 
1062
    def test_line_log_single_merge_revision(self):
 
1063
        wt = self._prepare_tree_with_merges()
 
1064
        logfile = self.make_utf8_encoded_stringio()
 
1065
        formatter = log.LineLogFormatter(to_file=logfile)
 
1066
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
 
1067
        wtb = wt.branch
 
1068
        rev = revspec.in_history(wtb)
 
1069
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
1070
        self.assertEqualDiff("""\
 
1071
1.1.1: Joe Foo 2005-11-22 rev-merged
 
1072
""",
 
1073
                             logfile.getvalue())
 
1074
 
 
1075
    def test_line_log_with_tags(self):
 
1076
        wt = self._prepare_tree_with_merges(with_tags=True)
 
1077
        logfile = self.make_utf8_encoded_stringio()
 
1078
        formatter = log.LineLogFormatter(to_file=logfile)
 
1079
        log.show_log(wt.branch, formatter)
 
1080
        self.assertEqualDiff("""\
 
1081
3: Jane Foo 2005-11-22 {v1.0, v1.0rc1} rev-3
 
1082
2: Joe Foo 2005-11-22 [merge] {v0.2} rev-2
 
1083
1: Joe Foo 2005-11-22 rev-1
 
1084
""",
 
1085
                             logfile.getvalue())
 
1086
 
 
1087
class TestLineLogFormatterWithMergeRevisions(tests.TestCaseWithTransport):
 
1088
 
 
1089
    def test_line_merge_revs_log(self):
 
1090
        """Line log should show revno
 
1091
 
 
1092
        bug #5162
 
1093
        """
 
1094
        wt = self.make_branch_and_tree('.')
 
1095
        b = wt.branch
 
1096
        self.build_tree(['a'])
 
1097
        wt.add('a')
 
1098
        b.nick = 'test-line-log'
 
1099
        wt.commit(message='add a',
 
1100
                  timestamp=1132711707,
 
1101
                  timezone=36000,
 
1102
                  committer='Line-Log-Formatter Tester <test@line.log>')
 
1103
        logfile = file('out.tmp', 'w+')
 
1104
        formatter = log.LineLogFormatter(to_file=logfile, levels=0)
 
1105
        log.show_log(b, formatter)
 
1106
        logfile.flush()
 
1107
        logfile.seek(0)
 
1108
        log_contents = logfile.read()
 
1109
        self.assertEqualDiff('1: Line-Log-Formatte... 2005-11-23 add a\n',
 
1110
                             log_contents)
 
1111
 
 
1112
    def test_line_merge_revs_log_single_merge_revision(self):
 
1113
        wt = self.make_branch_and_memory_tree('.')
 
1114
        wt.lock_write()
 
1115
        self.addCleanup(wt.unlock)
 
1116
        wt.add('')
 
1117
        wt.commit('rev-1', rev_id='rev-1',
 
1118
                  timestamp=1132586655, timezone=36000,
 
1119
                  committer='Joe Foo <joe@foo.com>')
 
1120
        wt.commit('rev-merged', rev_id='rev-2a',
 
1121
                  timestamp=1132586700, timezone=36000,
 
1122
                  committer='Joe Foo <joe@foo.com>')
 
1123
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
1124
        wt.branch.set_last_revision_info(1, 'rev-1')
 
1125
        wt.commit('rev-2', rev_id='rev-2b',
 
1126
                  timestamp=1132586800, timezone=36000,
 
1127
                  committer='Joe Foo <joe@foo.com>')
 
1128
        logfile = self.make_utf8_encoded_stringio()
 
1129
        formatter = log.LineLogFormatter(to_file=logfile, levels=0)
 
1130
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
 
1131
        wtb = wt.branch
 
1132
        rev = revspec.in_history(wtb)
 
1133
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
 
1134
        self.assertEqualDiff("""\
 
1135
1.1.1: Joe Foo 2005-11-22 rev-merged
 
1136
""",
 
1137
                             logfile.getvalue())
 
1138
 
 
1139
    def test_line_merge_revs_log_with_merges(self):
 
1140
        wt = self.make_branch_and_memory_tree('.')
 
1141
        wt.lock_write()
 
1142
        self.addCleanup(wt.unlock)
 
1143
        wt.add('')
 
1144
        wt.commit('rev-1', rev_id='rev-1',
 
1145
                  timestamp=1132586655, timezone=36000,
 
1146
                  committer='Joe Foo <joe@foo.com>')
 
1147
        wt.commit('rev-merged', rev_id='rev-2a',
 
1148
                  timestamp=1132586700, timezone=36000,
 
1149
                  committer='Joe Foo <joe@foo.com>')
 
1150
        wt.set_parent_ids(['rev-1', 'rev-2a'])
 
1151
        wt.branch.set_last_revision_info(1, 'rev-1')
 
1152
        wt.commit('rev-2', rev_id='rev-2b',
 
1153
                  timestamp=1132586800, timezone=36000,
 
1154
                  committer='Joe Foo <joe@foo.com>')
 
1155
        logfile = self.make_utf8_encoded_stringio()
 
1156
        formatter = log.LineLogFormatter(to_file=logfile, levels=0)
 
1157
        log.show_log(wt.branch, formatter)
 
1158
        self.assertEqualDiff("""\
 
1159
2: Joe Foo 2005-11-22 [merge] rev-2
 
1160
  1.1.1: Joe Foo 2005-11-22 rev-merged
 
1161
1: Joe Foo 2005-11-22 rev-1
 
1162
""",
 
1163
                             logfile.getvalue())
 
1164
 
 
1165
class TestGetViewRevisions(tests.TestCaseWithTransport):
288
1166
 
289
1167
    def make_tree_with_commits(self):
290
1168
        """Create a tree with well-known revision ids"""
311
1189
    def make_tree_with_many_merges(self):
312
1190
        """Create a tree with well-known revision ids"""
313
1191
        wt = self.make_branch_and_tree('tree1')
 
1192
        self.build_tree_contents([('tree1/f', '1\n')])
 
1193
        wt.add(['f'], ['f-id'])
314
1194
        wt.commit('commit one', rev_id='1')
315
1195
        wt.commit('commit two', rev_id='2')
 
1196
 
316
1197
        tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
 
1198
        self.build_tree_contents([('tree3/f', '1\n2\n3a\n')])
317
1199
        tree3.commit('commit three a', rev_id='3a')
 
1200
 
318
1201
        tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
319
1202
        tree2.merge_from_branch(tree3.branch)
320
1203
        tree2.commit('commit three b', rev_id='3b')
 
1204
 
321
1205
        wt.merge_from_branch(tree2.branch)
322
1206
        wt.commit('commit three c', rev_id='3c')
323
1207
        tree2.commit('four-a', rev_id='4a')
 
1208
 
324
1209
        wt.merge_from_branch(tree2.branch)
325
1210
        wt.commit('four-b', rev_id='4b')
 
1211
 
326
1212
        mainline_revs = [None, '1', '2', '3c', '4b']
327
1213
        rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
328
1214
        full_rev_nos_for_reference = {
329
1215
            '1': '1',
330
1216
            '2': '2',
331
 
            '3a': '2.2.1', #first commit tree 3
332
 
            '3b': '2.1.1', # first commit tree 2
 
1217
            '3a': '2.1.1', #first commit tree 3
 
1218
            '3b': '2.2.1', # first commit tree 2
333
1219
            '3c': '3', #merges 3b to main
334
 
            '4a': '2.1.2', # second commit tree 2
 
1220
            '4a': '2.2.2', # second commit tree 2
335
1221
            '4b': '4', # merges 4a to main
336
1222
            }
337
1223
        return mainline_revs, rev_nos, wt
339
1225
    def test_get_view_revisions_forward(self):
340
1226
        """Test the get_view_revisions method"""
341
1227
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
342
 
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
343
 
                                            'forward'))
 
1228
        wt.lock_read()
 
1229
        self.addCleanup(wt.unlock)
 
1230
        revisions = list(log.get_view_revisions(
 
1231
                mainline_revs, rev_nos, wt.branch, 'forward'))
344
1232
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
345
 
            revisions)
346
 
        revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
347
 
                                             'forward', include_merges=False))
 
1233
                         revisions)
 
1234
        revisions2 = list(log.get_view_revisions(
 
1235
                mainline_revs, rev_nos, wt.branch, 'forward',
 
1236
                include_merges=False))
348
1237
        self.assertEqual(revisions, revisions2)
349
1238
 
350
1239
    def test_get_view_revisions_reverse(self):
351
1240
        """Test the get_view_revisions with reverse"""
352
1241
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
353
 
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
354
 
                                            'reverse'))
 
1242
        wt.lock_read()
 
1243
        self.addCleanup(wt.unlock)
 
1244
        revisions = list(log.get_view_revisions(
 
1245
                mainline_revs, rev_nos, wt.branch, 'reverse'))
355
1246
        self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
356
 
            revisions)
357
 
        revisions2 = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
358
 
                                             'reverse', include_merges=False))
 
1247
                         revisions)
 
1248
        revisions2 = list(log.get_view_revisions(
 
1249
                mainline_revs, rev_nos, wt.branch, 'reverse',
 
1250
                include_merges=False))
359
1251
        self.assertEqual(revisions, revisions2)
360
1252
 
361
1253
    def test_get_view_revisions_merge(self):
362
1254
        """Test get_view_revisions when there are merges"""
363
1255
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
364
 
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
365
 
                                            'forward'))
366
 
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
367
 
            ('4b', '4', 0), ('4a', '3.1.1', 1)],
368
 
            revisions)
369
 
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
370
 
                                             'forward', include_merges=False))
371
 
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
372
 
            ('4b', '4', 0)],
373
 
            revisions)
 
1256
        wt.lock_read()
 
1257
        self.addCleanup(wt.unlock)
 
1258
        revisions = list(log.get_view_revisions(
 
1259
                mainline_revs, rev_nos, wt.branch, 'forward'))
 
1260
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
 
1261
                          ('4b', '4', 0), ('4a', '3.1.1', 1)],
 
1262
                         revisions)
 
1263
        revisions = list(log.get_view_revisions(
 
1264
                mainline_revs, rev_nos, wt.branch, 'forward',
 
1265
                include_merges=False))
 
1266
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
 
1267
                          ('4b', '4', 0)],
 
1268
                         revisions)
374
1269
 
375
1270
    def test_get_view_revisions_merge_reverse(self):
376
1271
        """Test get_view_revisions in reverse when there are merges"""
377
1272
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
378
 
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
379
 
                                            'reverse'))
 
1273
        wt.lock_read()
 
1274
        self.addCleanup(wt.unlock)
 
1275
        revisions = list(log.get_view_revisions(
 
1276
                mainline_revs, rev_nos, wt.branch, 'reverse'))
380
1277
        self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
381
 
            ('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
382
 
            revisions)
383
 
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
384
 
                                             'reverse', include_merges=False))
 
1278
                          ('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
 
1279
                         revisions)
 
1280
        revisions = list(log.get_view_revisions(
 
1281
                mainline_revs, rev_nos, wt.branch, 'reverse',
 
1282
                include_merges=False))
385
1283
        self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
386
 
            ('1', '1', 0)],
387
 
            revisions)
 
1284
                          ('1', '1', 0)],
 
1285
                         revisions)
388
1286
 
389
1287
    def test_get_view_revisions_merge2(self):
390
1288
        """Test get_view_revisions when there are merges"""
391
1289
        mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
392
 
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
393
 
                                            'forward'))
 
1290
        wt.lock_read()
 
1291
        self.addCleanup(wt.unlock)
 
1292
        revisions = list(log.get_view_revisions(
 
1293
                mainline_revs, rev_nos, wt.branch, 'forward'))
394
1294
        expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
395
 
            ('3a', '2.2.1', 1), ('3b', '2.1.1', 1), ('4b', '4', 0),
396
 
            ('4a', '2.1.2', 1)]
 
1295
                    ('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
 
1296
                    ('4a', '2.2.2', 1)]
397
1297
        self.assertEqual(expected, revisions)
398
 
        revisions = list(get_view_revisions(mainline_revs, rev_nos, wt.branch,
399
 
                                             'forward', include_merges=False))
 
1298
        revisions = list(log.get_view_revisions(
 
1299
                mainline_revs, rev_nos, wt.branch, 'forward',
 
1300
                include_merges=False))
400
1301
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
401
 
            ('4b', '4', 0)],
402
 
            revisions)
 
1302
                          ('4b', '4', 0)],
 
1303
                         revisions)
 
1304
 
 
1305
 
 
1306
    def test_file_id_for_range(self):
 
1307
        mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
 
1308
        wt.lock_read()
 
1309
        self.addCleanup(wt.unlock)
 
1310
 
 
1311
        def rev_from_rev_id(revid, branch):
 
1312
            revspec = revisionspec.RevisionSpec.from_string('revid:%s' % revid)
 
1313
            return revspec.in_history(branch)
 
1314
 
 
1315
        def view_revs(start_rev, end_rev, file_id, direction):
 
1316
            revs = log.calculate_view_revisions(
 
1317
                wt.branch,
 
1318
                start_rev, # start_revision
 
1319
                end_rev, # end_revision
 
1320
                direction, # direction
 
1321
                file_id, # specific_fileid
 
1322
                True, # generate_merge_revisions
 
1323
                )
 
1324
            return revs
 
1325
 
 
1326
        rev_3a = rev_from_rev_id('3a', wt.branch)
 
1327
        rev_4b = rev_from_rev_id('4b', wt.branch)
 
1328
        self.assertEqual([('3c', '3', 0), ('3a', '2.1.1', 1)],
 
1329
                          view_revs(rev_3a, rev_4b, 'f-id', 'reverse'))
 
1330
        # Note: 3c still appears before 3a here because of depth-based sorting
 
1331
        self.assertEqual([('3c', '3', 0), ('3a', '2.1.1', 1)],
 
1332
                          view_revs(rev_3a, rev_4b, 'f-id', 'forward'))
 
1333
 
 
1334
 
 
1335
class TestGetRevisionsTouchingFileID(tests.TestCaseWithTransport):
 
1336
 
 
1337
    def create_tree_with_single_merge(self):
 
1338
        """Create a branch with a moderate layout.
 
1339
 
 
1340
        The revision graph looks like:
 
1341
 
 
1342
           A
 
1343
           |\
 
1344
           B C
 
1345
           |/
 
1346
           D
 
1347
 
 
1348
        In this graph, A introduced files f1 and f2 and f3.
 
1349
        B modifies f1 and f3, and C modifies f2 and f3.
 
1350
        D merges the changes from B and C and resolves the conflict for f3.
 
1351
        """
 
1352
        # TODO: jam 20070218 This seems like it could really be done
 
1353
        #       with make_branch_and_memory_tree() if we could just
 
1354
        #       create the content of those files.
 
1355
        # TODO: jam 20070218 Another alternative is that we would really
 
1356
        #       like to only create this tree 1 time for all tests that
 
1357
        #       use it. Since 'log' only uses the tree in a readonly
 
1358
        #       fashion, it seems a shame to regenerate an identical
 
1359
        #       tree for each test.
 
1360
        tree = self.make_branch_and_tree('tree')
 
1361
        tree.lock_write()
 
1362
        self.addCleanup(tree.unlock)
 
1363
 
 
1364
        self.build_tree_contents([('tree/f1', 'A\n'),
 
1365
                                  ('tree/f2', 'A\n'),
 
1366
                                  ('tree/f3', 'A\n'),
 
1367
                                 ])
 
1368
        tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
 
1369
        tree.commit('A', rev_id='A')
 
1370
 
 
1371
        self.build_tree_contents([('tree/f2', 'A\nC\n'),
 
1372
                                  ('tree/f3', 'A\nC\n'),
 
1373
                                 ])
 
1374
        tree.commit('C', rev_id='C')
 
1375
        # Revert back to A to build the other history.
 
1376
        tree.set_last_revision('A')
 
1377
        tree.branch.set_last_revision_info(1, 'A')
 
1378
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
 
1379
                                  ('tree/f2', 'A\n'),
 
1380
                                  ('tree/f3', 'A\nB\n'),
 
1381
                                 ])
 
1382
        tree.commit('B', rev_id='B')
 
1383
        tree.set_parent_ids(['B', 'C'])
 
1384
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
 
1385
                                  ('tree/f2', 'A\nC\n'),
 
1386
                                  ('tree/f3', 'A\nB\nC\n'),
 
1387
                                 ])
 
1388
        tree.commit('D', rev_id='D')
 
1389
 
 
1390
        # Switch to a read lock for this tree.
 
1391
        # We still have an addCleanup(tree.unlock) pending
 
1392
        tree.unlock()
 
1393
        tree.lock_read()
 
1394
        return tree
 
1395
 
 
1396
    def check_delta(self, delta, **kw):
 
1397
        """Check the filenames touched by a delta are as expected.
 
1398
 
 
1399
        Caller only have to pass in the list of files for each part, all
 
1400
        unspecified parts are considered empty (and checked as such).
 
1401
        """
 
1402
        for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
 
1403
            # By default we expect an empty list
 
1404
            expected = kw.get(n, [])
 
1405
            # strip out only the path components
 
1406
            got = [x[0] for x in getattr(delta, n)]
 
1407
            self.assertEqual(expected, got)
 
1408
 
 
1409
    def test_tree_with_single_merge(self):
 
1410
        """Make sure the tree layout is correct."""
 
1411
        tree = self.create_tree_with_single_merge()
 
1412
        rev_A_tree = tree.branch.repository.revision_tree('A')
 
1413
        rev_B_tree = tree.branch.repository.revision_tree('B')
 
1414
        rev_C_tree = tree.branch.repository.revision_tree('C')
 
1415
        rev_D_tree = tree.branch.repository.revision_tree('D')
 
1416
 
 
1417
        self.check_delta(rev_B_tree.changes_from(rev_A_tree),
 
1418
                         modified=['f1', 'f3'])
 
1419
 
 
1420
        self.check_delta(rev_C_tree.changes_from(rev_A_tree),
 
1421
                         modified=['f2', 'f3'])
 
1422
 
 
1423
        self.check_delta(rev_D_tree.changes_from(rev_B_tree),
 
1424
                         modified=['f2', 'f3'])
 
1425
 
 
1426
        self.check_delta(rev_D_tree.changes_from(rev_C_tree),
 
1427
                         modified=['f1', 'f3'])
 
1428
 
 
1429
    def assertAllRevisionsForFileID(self, tree, file_id, revisions):
 
1430
        """Ensure _filter_revisions_touching_file_id returns the right values.
 
1431
 
 
1432
        Get the return value from _filter_revisions_touching_file_id and make
 
1433
        sure they are correct.
 
1434
        """
 
1435
        # The api for _filter_revisions_touching_file_id is a little crazy.
 
1436
        # So we do the setup here.
 
1437
        mainline = tree.branch.revision_history()
 
1438
        mainline.insert(0, None)
 
1439
        revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
 
1440
        view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
 
1441
                                                'reverse', True)
 
1442
        actual_revs = log._filter_revisions_touching_file_id(
 
1443
                            tree.branch,
 
1444
                            file_id,
 
1445
                            list(view_revs_iter))
 
1446
        self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
 
1447
 
 
1448
    def test_file_id_f1(self):
 
1449
        tree = self.create_tree_with_single_merge()
 
1450
        # f1 should be marked as modified by revisions A and B
 
1451
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
 
1452
 
 
1453
    def test_file_id_f2(self):
 
1454
        tree = self.create_tree_with_single_merge()
 
1455
        # f2 should be marked as modified by revisions A, C, and D
 
1456
        # because D merged the changes from C.
 
1457
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
 
1458
 
 
1459
    def test_file_id_f3(self):
 
1460
        tree = self.create_tree_with_single_merge()
 
1461
        # f3 should be marked as modified by revisions A, B, C, and D
 
1462
        self.assertAllRevisionsForFileID(tree, 'f3-id', ['D', 'C', 'B', 'A'])
 
1463
 
 
1464
    def test_file_id_with_ghosts(self):
 
1465
        # This is testing bug #209948, where having a ghost would cause
 
1466
        # _filter_revisions_touching_file_id() to fail.
 
1467
        tree = self.create_tree_with_single_merge()
 
1468
        # We need to add a revision, so switch back to a write-locked tree
 
1469
        # (still a single addCleanup(tree.unlock) pending).
 
1470
        tree.unlock()
 
1471
        tree.lock_write()
 
1472
        first_parent = tree.last_revision()
 
1473
        tree.set_parent_ids([first_parent, 'ghost-revision-id'])
 
1474
        self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
 
1475
        tree.commit('commit with a ghost', rev_id='XX')
 
1476
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
 
1477
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
 
1478
 
 
1479
    def test_unknown_file_id(self):
 
1480
        tree = self.create_tree_with_single_merge()
 
1481
        self.assertAllRevisionsForFileID(tree, 'unknown', [])
 
1482
 
 
1483
    def test_empty_branch_unknown_file_id(self):
 
1484
        tree = self.make_branch_and_tree('tree')
 
1485
        self.assertAllRevisionsForFileID(tree, 'unknown', [])
 
1486
 
 
1487
 
 
1488
class TestShowChangedRevisions(tests.TestCaseWithTransport):
 
1489
 
 
1490
    def test_show_changed_revisions_verbose(self):
 
1491
        tree = self.make_branch_and_tree('tree_a')
 
1492
        self.build_tree(['tree_a/foo'])
 
1493
        tree.add('foo')
 
1494
        tree.commit('bar', rev_id='bar-id')
 
1495
        s = self.make_utf8_encoded_stringio()
 
1496
        log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
 
1497
        self.assertContainsRe(s.getvalue(), 'bar')
 
1498
        self.assertNotContainsRe(s.getvalue(), 'foo')
 
1499
 
 
1500
 
 
1501
class TestLogFormatter(tests.TestCase):
 
1502
 
 
1503
    def test_short_committer(self):
 
1504
        rev = revision.Revision('a-id')
 
1505
        rev.committer = 'John Doe <jdoe@example.com>'
 
1506
        lf = log.LogFormatter(None)
 
1507
        self.assertEqual('John Doe', lf.short_committer(rev))
 
1508
        rev.committer = 'John Smith <jsmith@example.com>'
 
1509
        self.assertEqual('John Smith', lf.short_committer(rev))
 
1510
        rev.committer = 'John Smith'
 
1511
        self.assertEqual('John Smith', lf.short_committer(rev))
 
1512
        rev.committer = 'jsmith@example.com'
 
1513
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
 
1514
        rev.committer = '<jsmith@example.com>'
 
1515
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
 
1516
        rev.committer = 'John Smith jsmith@example.com'
 
1517
        self.assertEqual('John Smith', lf.short_committer(rev))
 
1518
 
 
1519
    def test_short_author(self):
 
1520
        rev = revision.Revision('a-id')
 
1521
        rev.committer = 'John Doe <jdoe@example.com>'
 
1522
        lf = log.LogFormatter(None)
 
1523
        self.assertEqual('John Doe', lf.short_author(rev))
 
1524
        rev.properties['author'] = 'John Smith <jsmith@example.com>'
 
1525
        self.assertEqual('John Smith', lf.short_author(rev))
 
1526
        rev.properties['author'] = 'John Smith'
 
1527
        self.assertEqual('John Smith', lf.short_author(rev))
 
1528
        rev.properties['author'] = 'jsmith@example.com'
 
1529
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
 
1530
        rev.properties['author'] = '<jsmith@example.com>'
 
1531
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
 
1532
        rev.properties['author'] = 'John Smith jsmith@example.com'
 
1533
        self.assertEqual('John Smith', lf.short_author(rev))
 
1534
        del rev.properties['author']
 
1535
        rev.properties['authors'] = ('John Smith <jsmith@example.com>\n'
 
1536
                'Jane Rey <jrey@example.com>')
 
1537
        self.assertEqual('John Smith', lf.short_author(rev))
 
1538
 
 
1539
 
 
1540
class TestReverseByDepth(tests.TestCase):
 
1541
    """Test reverse_by_depth behavior.
 
1542
 
 
1543
    This is used to present revisions in forward (oldest first) order in a nice
 
1544
    layout.
 
1545
 
 
1546
    The tests use lighter revision description to ease reading.
 
1547
    """
 
1548
 
 
1549
    def assertReversed(self, forward, backward):
 
1550
        # Transform the descriptions to suit the API: tests use (revno, depth),
 
1551
        # while the API expects (revid, revno, depth)
 
1552
        def complete_revisions(l):
 
1553
            """Transform the description to suit the API.
 
1554
 
 
1555
            Tests use (revno, depth) whil the API expects (revid, revno, depth).
 
1556
            Since the revid is arbitrary, we just duplicate revno
 
1557
            """
 
1558
            return [ (r, r, d) for r, d in l]
 
1559
        forward = complete_revisions(forward)
 
1560
        backward= complete_revisions(backward)
 
1561
        self.assertEqual(forward, log.reverse_by_depth(backward))
 
1562
 
 
1563
 
 
1564
    def test_mainline_revisions(self):
 
1565
        self.assertReversed([( '1', 0), ('2', 0)],
 
1566
                            [('2', 0), ('1', 0)])
 
1567
 
 
1568
    def test_merged_revisions(self):
 
1569
        self.assertReversed([('1', 0), ('2', 0), ('2.2', 1), ('2.1', 1),],
 
1570
                            [('2', 0), ('2.1', 1), ('2.2', 1), ('1', 0),])
 
1571
    def test_shifted_merged_revisions(self):
 
1572
        """Test irregular layout.
 
1573
 
 
1574
        Requesting revisions touching a file can produce "holes" in the depths.
 
1575
        """
 
1576
        self.assertReversed([('1', 0), ('2', 0), ('1.1', 2), ('1.2', 2),],
 
1577
                            [('2', 0), ('1.2', 2), ('1.1', 2), ('1', 0),])
 
1578
 
 
1579
    def test_merged_without_child_revisions(self):
 
1580
        """Test irregular layout.
 
1581
 
 
1582
        Revision ranges can produce "holes" in the depths.
 
1583
        """
 
1584
        # When a revision of higher depth doesn't follow one of lower depth, we
 
1585
        # assume a lower depth one is virtually there
 
1586
        self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
 
1587
                            [('4', 4), ('3', 3), ('2', 2), ('1', 2),])
 
1588
        # So we get the same order after reversing below even if the original
 
1589
        # revisions are not in the same order.
 
1590
        self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
 
1591
                            [('3', 3), ('4', 4), ('2', 2), ('1', 2),])
 
1592
 
 
1593
 
 
1594
class TestHistoryChange(tests.TestCaseWithTransport):
 
1595
 
 
1596
    def setup_a_tree(self):
 
1597
        tree = self.make_branch_and_tree('tree')
 
1598
        tree.lock_write()
 
1599
        self.addCleanup(tree.unlock)
 
1600
        tree.commit('1a', rev_id='1a')
 
1601
        tree.commit('2a', rev_id='2a')
 
1602
        tree.commit('3a', rev_id='3a')
 
1603
        return tree
 
1604
 
 
1605
    def setup_ab_tree(self):
 
1606
        tree = self.setup_a_tree()
 
1607
        tree.set_last_revision('1a')
 
1608
        tree.branch.set_last_revision_info(1, '1a')
 
1609
        tree.commit('2b', rev_id='2b')
 
1610
        tree.commit('3b', rev_id='3b')
 
1611
        return tree
 
1612
 
 
1613
    def setup_ac_tree(self):
 
1614
        tree = self.setup_a_tree()
 
1615
        tree.set_last_revision(revision.NULL_REVISION)
 
1616
        tree.branch.set_last_revision_info(0, revision.NULL_REVISION)
 
1617
        tree.commit('1c', rev_id='1c')
 
1618
        tree.commit('2c', rev_id='2c')
 
1619
        tree.commit('3c', rev_id='3c')
 
1620
        return tree
 
1621
 
 
1622
    def test_all_new(self):
 
1623
        tree = self.setup_ab_tree()
 
1624
        old, new = log.get_history_change('1a', '3a', tree.branch.repository)
 
1625
        self.assertEqual([], old)
 
1626
        self.assertEqual(['2a', '3a'], new)
 
1627
 
 
1628
    def test_all_old(self):
 
1629
        tree = self.setup_ab_tree()
 
1630
        old, new = log.get_history_change('3a', '1a', tree.branch.repository)
 
1631
        self.assertEqual([], new)
 
1632
        self.assertEqual(['2a', '3a'], old)
 
1633
 
 
1634
    def test_null_old(self):
 
1635
        tree = self.setup_ab_tree()
 
1636
        old, new = log.get_history_change(revision.NULL_REVISION,
 
1637
                                          '3a', tree.branch.repository)
 
1638
        self.assertEqual([], old)
 
1639
        self.assertEqual(['1a', '2a', '3a'], new)
 
1640
 
 
1641
    def test_null_new(self):
 
1642
        tree = self.setup_ab_tree()
 
1643
        old, new = log.get_history_change('3a', revision.NULL_REVISION,
 
1644
                                          tree.branch.repository)
 
1645
        self.assertEqual([], new)
 
1646
        self.assertEqual(['1a', '2a', '3a'], old)
 
1647
 
 
1648
    def test_diverged(self):
 
1649
        tree = self.setup_ab_tree()
 
1650
        old, new = log.get_history_change('3a', '3b', tree.branch.repository)
 
1651
        self.assertEqual(old, ['2a', '3a'])
 
1652
        self.assertEqual(new, ['2b', '3b'])
 
1653
 
 
1654
    def test_unrelated(self):
 
1655
        tree = self.setup_ac_tree()
 
1656
        old, new = log.get_history_change('3a', '3c', tree.branch.repository)
 
1657
        self.assertEqual(old, ['1a', '2a', '3a'])
 
1658
        self.assertEqual(new, ['1c', '2c', '3c'])
 
1659
 
 
1660
    def test_show_branch_change(self):
 
1661
        tree = self.setup_ab_tree()
 
1662
        s = StringIO()
 
1663
        log.show_branch_change(tree.branch, s, 3, '3a')
 
1664
        self.assertContainsRe(s.getvalue(),
 
1665
            '[*]{60}\nRemoved Revisions:\n(.|\n)*2a(.|\n)*3a(.|\n)*'
 
1666
            '[*]{60}\n\nAdded Revisions:\n(.|\n)*2b(.|\n)*3b')
 
1667
 
 
1668
    def test_show_branch_change_no_change(self):
 
1669
        tree = self.setup_ab_tree()
 
1670
        s = StringIO()
 
1671
        log.show_branch_change(tree.branch, s, 3, '3b')
 
1672
        self.assertEqual(s.getvalue(),
 
1673
            'Nothing seems to have changed\n')
 
1674
 
 
1675
    def test_show_branch_change_no_old(self):
 
1676
        tree = self.setup_ab_tree()
 
1677
        s = StringIO()
 
1678
        log.show_branch_change(tree.branch, s, 2, '2b')
 
1679
        self.assertContainsRe(s.getvalue(), 'Added Revisions:')
 
1680
        self.assertNotContainsRe(s.getvalue(), 'Removed Revisions:')
 
1681
 
 
1682
    def test_show_branch_change_no_new(self):
 
1683
        tree = self.setup_ab_tree()
 
1684
        tree.branch.set_last_revision_info(2, '2b')
 
1685
        s = StringIO()
 
1686
        log.show_branch_change(tree.branch, s, 3, '3b')
 
1687
        self.assertContainsRe(s.getvalue(), 'Removed Revisions:')
 
1688
        self.assertNotContainsRe(s.getvalue(), 'Added Revisions:')