~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_log.py

  • Committer: Robert Collins
  • Date: 2006-02-26 07:54:02 UTC
  • mto: (1587.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 1588.
  • Revision ID: robertc@robertcollins.net-20060226075402-92fca9fdb7b0070d
Check for incorrect revision parentage in the weave during revision access.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2005 by Canonical Ltd
 
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
17
import os
18
18
from cStringIO import StringIO
19
19
 
20
 
from bzrlib import (
21
 
    errors,
22
 
    log,
23
 
    registry,
24
 
    revision,
25
 
    revisionspec,
26
 
    tests,
27
 
    )
28
 
 
29
 
 
30
 
class TestCaseWithoutPropsHandler(tests.TestCaseWithTransport):
31
 
 
32
 
    def setUp(self):
33
 
        super(TestCaseWithoutPropsHandler, self).setUp()
34
 
        # keep a reference to the "current" custom prop. handler registry
35
 
        self.properties_handler_registry = log.properties_handler_registry
36
 
        # clean up the registry in log
37
 
        log.properties_handler_registry = registry.Registry()
38
 
 
39
 
    def _cleanup(self):
40
 
        super(TestCaseWithoutPropsHandler, self)._cleanup()
41
 
        # restore the custom properties handler registry
42
 
        log.properties_handler_registry = self.properties_handler_registry
43
 
 
44
 
 
45
 
class LogCatcher(log.LogFormatter):
 
20
from bzrlib.tests import BzrTestBase, TestCaseWithTransport
 
21
from bzrlib.log import LogFormatter, show_log, LongLogFormatter, ShortLogFormatter
 
22
from bzrlib.branch import Branch
 
23
from bzrlib.errors import InvalidRevisionNumber
 
24
 
 
25
class _LogEntry(object):
 
26
    # should probably move into bzrlib.log?
 
27
    pass
 
28
 
 
29
 
 
30
class LogCatcher(LogFormatter):
46
31
    """Pull log messages into list rather than displaying them.
47
32
 
48
33
    For ease of testing we save log messages here rather than actually
51
36
 
52
37
    We should also test the LogFormatter.
53
38
    """
54
 
 
55
 
    supports_delta = True
56
 
 
57
39
    def __init__(self):
58
40
        super(LogCatcher, self).__init__(to_file=None)
59
41
        self.logs = []
60
 
 
61
 
    def log_revision(self, revision):
62
 
        self.logs.append(revision)
63
 
 
64
 
 
65
 
class TestShowLog(tests.TestCaseWithTransport):
 
42
        
 
43
        
 
44
    def show(self, revno, rev, delta):
 
45
        le = _LogEntry()
 
46
        le.revno = revno
 
47
        le.rev = rev
 
48
        le.delta = delta
 
49
        self.logs.append(le)
 
50
 
 
51
 
 
52
class SimpleLogTest(TestCaseWithTransport):
66
53
 
67
54
    def checkDelta(self, delta, **kw):
68
 
        """Check the filenames touched by a delta are as expected.
69
 
 
70
 
        Caller only have to pass in the list of files for each part, all
71
 
        unspecified parts are considered empty (and checked as such).
72
 
        """
 
55
        """Check the filenames touched by a delta are as expected."""
73
56
        for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
74
 
            # By default we expect an empty list
75
57
            expected = kw.get(n, [])
 
58
 
 
59
            # tests are written with unix paths; fix them up for windows
 
60
            #if os.sep != '/':
 
61
            #    expected = [x.replace('/', os.sep) for x in expected]
 
62
 
76
63
            # strip out only the path components
77
64
            got = [x[0] for x in getattr(delta, n)]
78
 
            self.assertEqual(expected, got)
79
 
 
80
 
    def assertInvalidRevisonNumber(self, br, start, end):
81
 
        lf = LogCatcher()
82
 
        self.assertRaises(errors.InvalidRevisionNumber,
83
 
                          log.show_log, br, lf,
84
 
                          start_revision=start, end_revision=end)
85
 
 
86
 
    def test_cur_revno(self):
87
 
        wt = self.make_branch_and_tree('.')
88
 
        b = wt.branch
89
 
 
90
 
        lf = LogCatcher()
91
 
        wt.commit('empty commit')
92
 
        log.show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
93
 
 
94
 
        # Since there is a single revision in the branch all the combinations
95
 
        # below should fail.
96
 
        self.assertInvalidRevisonNumber(b, 2, 1)
97
 
        self.assertInvalidRevisonNumber(b, 1, 2)
98
 
        self.assertInvalidRevisonNumber(b, 0, 2)
99
 
        self.assertInvalidRevisonNumber(b, 1, 0)
100
 
        self.assertInvalidRevisonNumber(b, -1, 1)
101
 
        self.assertInvalidRevisonNumber(b, 1, -1)
102
 
 
103
 
    def test_empty_branch(self):
104
 
        wt = self.make_branch_and_tree('.')
105
 
 
106
 
        lf = LogCatcher()
107
 
        log.show_log(wt.branch, lf)
 
65
            self.assertEquals(expected, got)
 
66
 
 
67
    def test_cur_revno(self):
 
68
        wt = self.make_branch_and_tree('.')
 
69
        b = wt.branch
 
70
 
 
71
        lf = LogCatcher()
 
72
        wt.commit('empty commit')
 
73
        show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
 
74
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
75
                          start_revision=2, end_revision=1) 
 
76
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
77
                          start_revision=1, end_revision=2) 
 
78
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
79
                          start_revision=0, end_revision=2) 
 
80
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
81
                          start_revision=1, end_revision=0) 
 
82
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
83
                          start_revision=-1, end_revision=1) 
 
84
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
85
                          start_revision=1, end_revision=-1) 
 
86
 
 
87
    def test_cur_revno(self):
 
88
        wt = self.make_branch_and_tree('.')
 
89
        b = wt.branch
 
90
 
 
91
        lf = LogCatcher()
 
92
        wt.commit('empty commit')
 
93
        show_log(b, lf, verbose=True, start_revision=1, end_revision=1)
 
94
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
95
                          start_revision=2, end_revision=1) 
 
96
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
97
                          start_revision=1, end_revision=2) 
 
98
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
99
                          start_revision=0, end_revision=2) 
 
100
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
101
                          start_revision=1, end_revision=0) 
 
102
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
103
                          start_revision=-1, end_revision=1) 
 
104
        self.assertRaises(InvalidRevisionNumber, show_log, b, lf,
 
105
                          start_revision=1, end_revision=-1) 
 
106
 
 
107
    def test_simple_log(self):
 
108
        eq = self.assertEquals
 
109
        
 
110
        wt = self.make_branch_and_tree('.')
 
111
        b = wt.branch
 
112
 
 
113
        lf = LogCatcher()
 
114
        show_log(b, lf)
108
115
        # no entries yet
109
 
        self.assertEqual([], lf.logs)
110
 
 
111
 
    def test_empty_commit(self):
112
 
        wt = self.make_branch_and_tree('.')
 
116
        eq(lf.logs, [])
113
117
 
114
118
        wt.commit('empty commit')
115
119
        lf = LogCatcher()
116
 
        log.show_log(wt.branch, lf, verbose=True)
117
 
        self.assertEqual(1, len(lf.logs))
118
 
        self.assertEqual('1', lf.logs[0].revno)
119
 
        self.assertEqual('empty commit', lf.logs[0].rev.message)
120
 
        self.checkDelta(lf.logs[0].delta)
 
120
        show_log(b, lf, verbose=True)
 
121
        eq(len(lf.logs), 1)
 
122
        eq(lf.logs[0].revno, 1)
 
123
        eq(lf.logs[0].rev.message, 'empty commit')
 
124
        d = lf.logs[0].delta
 
125
        self.log('log delta: %r' % d)
 
126
        self.checkDelta(d)
121
127
 
122
 
    def test_simple_commit(self):
123
 
        wt = self.make_branch_and_tree('.')
124
 
        wt.commit('empty commit')
125
128
        self.build_tree(['hello'])
126
129
        wt.add('hello')
127
 
        wt.commit('add one file',
128
 
                  committer=u'\u013d\xf3r\xe9m \xcdp\u0161\xfam '
129
 
                            u'<test@example.com>')
 
130
        wt.commit('add one file')
 
131
 
 
132
        lf = StringIO()
 
133
        # log using regular thing
 
134
        show_log(b, LongLogFormatter(lf))
 
135
        lf.seek(0)
 
136
        for l in lf.readlines():
 
137
            self.log(l)
 
138
 
 
139
        # get log as data structure
130
140
        lf = LogCatcher()
131
 
        log.show_log(wt.branch, lf, verbose=True)
132
 
        self.assertEqual(2, len(lf.logs))
 
141
        show_log(b, lf, verbose=True)
 
142
        eq(len(lf.logs), 2)
 
143
        self.log('log entries:')
 
144
        for logentry in lf.logs:
 
145
            self.log('%4d %s' % (logentry.revno, logentry.rev.message))
 
146
        
133
147
        # first one is most recent
134
 
        log_entry = lf.logs[0]
135
 
        self.assertEqual('2', log_entry.revno)
136
 
        self.assertEqual('add one file', log_entry.rev.message)
137
 
        self.checkDelta(log_entry.delta, added=['hello'])
138
 
 
139
 
    def test_commit_message_with_control_chars(self):
140
 
        wt = self.make_branch_and_tree('.')
141
 
        msg = u"All 8-bit chars: " +  ''.join([unichr(x) for x in range(256)])
142
 
        msg = msg.replace(u'\r', u'\n')
 
148
        logentry = lf.logs[0]
 
149
        eq(logentry.revno, 2)
 
150
        eq(logentry.rev.message, 'add one file')
 
151
        d = logentry.delta
 
152
        self.log('log 2 delta: %r' % d)
 
153
        # self.checkDelta(d, added=['hello'])
 
154
        
 
155
        # commit a log message with control characters
 
156
        msg = "All 8-bit chars: " +  ''.join([unichr(x) for x in range(256)])
 
157
        self.log("original commit message: %r", msg)
143
158
        wt.commit(msg)
144
159
        lf = LogCatcher()
145
 
        log.show_log(wt.branch, lf, verbose=True)
 
160
        show_log(b, lf, verbose=True)
146
161
        committed_msg = lf.logs[0].rev.message
147
 
        self.assertNotEqual(msg, committed_msg)
148
 
        self.assertTrue(len(committed_msg) > len(msg))
 
162
        self.log("escaped commit message: %r", committed_msg)
 
163
        self.assert_(msg != committed_msg)
 
164
        self.assert_(len(committed_msg) > len(msg))
149
165
 
150
 
    def test_commit_message_without_control_chars(self):
151
 
        wt = self.make_branch_and_tree('.')
 
166
        # Check that log message with only XML-valid characters isn't
152
167
        # escaped.  As ElementTree apparently does some kind of
153
168
        # newline conversion, neither LF (\x0A) nor CR (\x0D) are
154
169
        # included in the test commit message, even though they are
155
170
        # valid XML 1.0 characters.
156
171
        msg = "\x09" + ''.join([unichr(x) for x in range(0x20, 256)])
 
172
        self.log("original commit message: %r", msg)
157
173
        wt.commit(msg)
158
174
        lf = LogCatcher()
159
 
        log.show_log(wt.branch, lf, verbose=True)
 
175
        show_log(b, lf, verbose=True)
160
176
        committed_msg = lf.logs[0].rev.message
161
 
        self.assertEqual(msg, committed_msg)
162
 
 
163
 
    def test_deltas_in_merge_revisions(self):
164
 
        """Check deltas created for both mainline and merge revisions"""
165
 
        wt = self.make_branch_and_tree('parent')
166
 
        self.build_tree(['parent/file1', 'parent/file2', 'parent/file3'])
167
 
        wt.add('file1')
168
 
        wt.add('file2')
169
 
        wt.commit(message='add file1 and file2')
170
 
        self.run_bzr('branch parent child')
171
 
        os.unlink('child/file1')
172
 
        file('child/file2', 'wb').write('hello\n')
173
 
        self.run_bzr(['commit', '-m', 'remove file1 and modify file2',
174
 
            'child'])
175
 
        os.chdir('parent')
176
 
        self.run_bzr('merge ../child')
177
 
        wt.commit('merge child branch')
178
 
        os.chdir('..')
179
 
        b = wt.branch
180
 
        lf = LogCatcher()
181
 
        lf.supports_merge_revisions = True
182
 
        log.show_log(b, lf, verbose=True)
183
 
 
184
 
        self.assertEqual(3, len(lf.logs))
185
 
 
186
 
        logentry = lf.logs[0]
187
 
        self.assertEqual('2', logentry.revno)
188
 
        self.assertEqual('merge child branch', logentry.rev.message)
189
 
        self.checkDelta(logentry.delta, removed=['file1'], modified=['file2'])
190
 
 
191
 
        logentry = lf.logs[1]
192
 
        self.assertEqual('1.1.1', logentry.revno)
193
 
        self.assertEqual('remove file1 and modify file2', logentry.rev.message)
194
 
        self.checkDelta(logentry.delta, removed=['file1'], modified=['file2'])
195
 
 
196
 
        logentry = lf.logs[2]
197
 
        self.assertEqual('1', logentry.revno)
198
 
        self.assertEqual('add file1 and file2', logentry.rev.message)
199
 
        self.checkDelta(logentry.delta, added=['file1', 'file2'])
200
 
 
201
 
    def test_merges_nonsupporting_formatter(self):
202
 
        """Tests that show_log will raise if the formatter doesn't
203
 
        support merge revisions."""
204
 
        wt = self.make_branch_and_memory_tree('.')
205
 
        wt.lock_write()
206
 
        self.addCleanup(wt.unlock)
207
 
        wt.add('')
208
 
        wt.commit('rev-1', rev_id='rev-1',
209
 
                  timestamp=1132586655, timezone=36000,
210
 
                  committer='Joe Foo <joe@foo.com>')
211
 
        wt.commit('rev-merged', rev_id='rev-2a',
212
 
                  timestamp=1132586700, timezone=36000,
213
 
                  committer='Joe Foo <joe@foo.com>')
214
 
        wt.set_parent_ids(['rev-1', 'rev-2a'])
215
 
        wt.branch.set_last_revision_info(1, 'rev-1')
216
 
        wt.commit('rev-2', rev_id='rev-2b',
217
 
                  timestamp=1132586800, timezone=36000,
218
 
                  committer='Joe Foo <joe@foo.com>')
219
 
        logfile = self.make_utf8_encoded_stringio()
220
 
        formatter = log.ShortLogFormatter(to_file=logfile)
221
 
        wtb = wt.branch
222
 
        lf = LogCatcher()
223
 
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
224
 
        rev = revspec.in_history(wtb)
225
 
        self.assertRaises(errors.BzrCommandError, log.show_log, wtb, lf,
226
 
                          start_revision=rev, end_revision=rev)
227
 
 
228
 
 
229
 
def make_commits_with_trailing_newlines(wt):
230
 
    """Helper method for LogFormatter tests"""
231
 
    b = wt.branch
232
 
    b.nick='test'
233
 
    open('a', 'wb').write('hello moto\n')
234
 
    wt.add('a')
235
 
    wt.commit('simple log message', rev_id='a1',
236
 
              timestamp=1132586655.459960938, timezone=-6*3600,
237
 
              committer='Joe Foo <joe@foo.com>')
238
 
    open('b', 'wb').write('goodbye\n')
239
 
    wt.add('b')
240
 
    wt.commit('multiline\nlog\nmessage\n', rev_id='a2',
241
 
              timestamp=1132586842.411175966, timezone=-6*3600,
242
 
              committer='Joe Foo <joe@foo.com>',
243
 
              author='Joe Bar <joe@bar.com>')
244
 
 
245
 
    open('c', 'wb').write('just another manic monday\n')
246
 
    wt.add('c')
247
 
    wt.commit('single line with trailing newline\n', rev_id='a3',
248
 
              timestamp=1132587176.835228920, timezone=-6*3600,
249
 
              committer = 'Joe Foo <joe@foo.com>')
250
 
    return b
251
 
 
252
 
 
253
 
def normalize_log(log):
254
 
    """Replaces the variable lines of logs with fixed lines"""
255
 
    author = 'author: Dolor Sit <test@example.com>'
256
 
    committer = 'committer: Lorem Ipsum <test@example.com>'
257
 
    lines = log.splitlines(True)
258
 
    for idx,line in enumerate(lines):
259
 
        stripped_line = line.lstrip()
260
 
        indent = ' ' * (len(line) - len(stripped_line))
261
 
        if stripped_line.startswith('author:'):
262
 
            lines[idx] = indent + author + '\n'
263
 
        elif stripped_line.startswith('committer:'):
264
 
            lines[idx] = indent + committer + '\n'
265
 
        elif stripped_line.startswith('timestamp:'):
266
 
            lines[idx] = indent + 'timestamp: Just now\n'
267
 
    return ''.join(lines)
268
 
 
269
 
 
270
 
class TestShortLogFormatter(tests.TestCaseWithTransport):
 
177
        self.log("escaped commit message: %r", committed_msg)
 
178
        self.assert_(msg == committed_msg)
271
179
 
272
180
    def test_trailing_newlines(self):
273
181
        wt = self.make_branch_and_tree('.')
274
 
        b = make_commits_with_trailing_newlines(wt)
275
 
        sio = self.make_utf8_encoded_stringio()
276
 
        lf = log.ShortLogFormatter(to_file=sio)
277
 
        log.show_log(b, lf)
278
 
        self.assertEqualDiff("""\
 
182
        b = wt.branch
 
183
        b.nick='test'
 
184
        open('a', 'wb').write('hello moto\n')
 
185
        wt.add('a')
 
186
        wt.commit('simple log message', rev_id='a1'
 
187
                , timestamp=1132586655.459960938, timezone=-6*3600
 
188
                , committer='Joe Foo <joe@foo.com>')
 
189
        open('b', 'wb').write('goodbye\n')
 
190
        wt.add('b')
 
191
        wt.commit('multiline\nlog\nmessage\n', rev_id='a2'
 
192
                , timestamp=1132586842.411175966, timezone=-6*3600
 
193
                , committer='Joe Foo <joe@foo.com>')
 
194
 
 
195
        open('c', 'wb').write('just another manic monday\n')
 
196
        wt.add('c')
 
197
        wt.commit('single line with trailing newline\n', rev_id='a3'
 
198
                , timestamp=1132587176.835228920, timezone=-6*3600
 
199
                , committer = 'Joe Foo <joe@foo.com>')
 
200
 
 
201
        sio = StringIO()
 
202
        lf = ShortLogFormatter(to_file=sio)
 
203
        show_log(b, lf)
 
204
        self.assertEquals(sio.getvalue(), """\
279
205
    3 Joe Foo\t2005-11-21
280
206
      single line with trailing newline
281
207
 
282
 
    2 Joe Bar\t2005-11-21
 
208
    2 Joe Foo\t2005-11-21
283
209
      multiline
284
210
      log
285
211
      message
287
213
    1 Joe Foo\t2005-11-21
288
214
      simple log message
289
215
 
290
 
""",
291
 
                             sio.getvalue())
292
 
 
293
 
    def test_short_log_with_merges(self):
294
 
        wt = self.make_branch_and_memory_tree('.')
295
 
        wt.lock_write()
296
 
        self.addCleanup(wt.unlock)
297
 
        wt.add('')
298
 
        wt.commit('rev-1', rev_id='rev-1',
299
 
                  timestamp=1132586655, timezone=36000,
300
 
                  committer='Joe Foo <joe@foo.com>')
301
 
        wt.commit('rev-merged', rev_id='rev-2a',
302
 
                  timestamp=1132586700, timezone=36000,
303
 
                  committer='Joe Foo <joe@foo.com>')
304
 
        wt.set_parent_ids(['rev-1', 'rev-2a'])
305
 
        wt.branch.set_last_revision_info(1, 'rev-1')
306
 
        wt.commit('rev-2', rev_id='rev-2b',
307
 
                  timestamp=1132586800, timezone=36000,
308
 
                  committer='Joe Foo <joe@foo.com>')
309
 
        logfile = self.make_utf8_encoded_stringio()
310
 
        formatter = log.ShortLogFormatter(to_file=logfile)
311
 
        log.show_log(wt.branch, formatter)
312
 
        self.assertEqualDiff("""\
313
 
    2 Joe Foo\t2005-11-22 [merge]
314
 
      rev-2
315
 
 
316
 
    1 Joe Foo\t2005-11-22
317
 
      rev-1
318
 
 
319
 
""",
320
 
                             logfile.getvalue())
321
 
 
322
 
    def test_short_log_single_merge_revision(self):
323
 
        wt = self.make_branch_and_memory_tree('.')
324
 
        wt.lock_write()
325
 
        self.addCleanup(wt.unlock)
326
 
        wt.add('')
327
 
        wt.commit('rev-1', rev_id='rev-1',
328
 
                  timestamp=1132586655, timezone=36000,
329
 
                  committer='Joe Foo <joe@foo.com>')
330
 
        wt.commit('rev-merged', rev_id='rev-2a',
331
 
                  timestamp=1132586700, timezone=36000,
332
 
                  committer='Joe Foo <joe@foo.com>')
333
 
        wt.set_parent_ids(['rev-1', 'rev-2a'])
334
 
        wt.branch.set_last_revision_info(1, 'rev-1')
335
 
        wt.commit('rev-2', rev_id='rev-2b',
336
 
                  timestamp=1132586800, timezone=36000,
337
 
                  committer='Joe Foo <joe@foo.com>')
338
 
        logfile = self.make_utf8_encoded_stringio()
339
 
        formatter = log.ShortLogFormatter(to_file=logfile)
340
 
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
341
 
        wtb = wt.branch
342
 
        rev = revspec.in_history(wtb)
343
 
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
344
 
        self.assertEqualDiff("""\
345
 
1.1.1 Joe Foo\t2005-11-22
346
 
      rev-merged
347
 
 
348
 
""",
349
 
                             logfile.getvalue())
350
 
 
351
 
 
352
 
class TestLongLogFormatter(TestCaseWithoutPropsHandler):
353
 
 
354
 
    def test_verbose_log(self):
355
 
        """Verbose log includes changed files
356
 
        
357
 
        bug #4676
358
 
        """
359
 
        wt = self.make_branch_and_tree('.')
360
 
        b = wt.branch
361
 
        self.build_tree(['a'])
362
 
        wt.add('a')
363
 
        # XXX: why does a longer nick show up?
364
 
        b.nick = 'test_verbose_log'
365
 
        wt.commit(message='add a',
366
 
                  timestamp=1132711707,
367
 
                  timezone=36000,
368
 
                  committer='Lorem Ipsum <test@example.com>')
369
 
        logfile = file('out.tmp', 'w+')
370
 
        formatter = log.LongLogFormatter(to_file=logfile)
371
 
        log.show_log(b, formatter, verbose=True)
372
 
        logfile.flush()
373
 
        logfile.seek(0)
374
 
        log_contents = logfile.read()
375
 
        self.assertEqualDiff('''\
376
 
------------------------------------------------------------
377
 
revno: 1
378
 
committer: Lorem Ipsum <test@example.com>
379
 
branch nick: test_verbose_log
380
 
timestamp: Wed 2005-11-23 12:08:27 +1000
381
 
message:
382
 
  add a
383
 
added:
384
 
  a
385
 
''',
386
 
                             log_contents)
387
 
 
388
 
    def test_merges_are_indented_by_level(self):
389
 
        wt = self.make_branch_and_tree('parent')
390
 
        wt.commit('first post')
391
 
        self.run_bzr('branch parent child')
392
 
        self.run_bzr(['commit', '-m', 'branch 1', '--unchanged', 'child'])
393
 
        self.run_bzr('branch child smallerchild')
394
 
        self.run_bzr(['commit', '-m', 'branch 2', '--unchanged',
395
 
            'smallerchild'])
396
 
        os.chdir('child')
397
 
        self.run_bzr('merge ../smallerchild')
398
 
        self.run_bzr(['commit', '-m', 'merge branch 2'])
399
 
        os.chdir('../parent')
400
 
        self.run_bzr('merge ../child')
401
 
        wt.commit('merge branch 1')
402
 
        b = wt.branch
403
 
        sio = self.make_utf8_encoded_stringio()
404
 
        lf = log.LongLogFormatter(to_file=sio)
405
 
        log.show_log(b, lf, verbose=True)
406
 
        the_log = normalize_log(sio.getvalue())
407
 
        self.assertEqualDiff("""\
408
 
------------------------------------------------------------
409
 
revno: 2
410
 
committer: Lorem Ipsum <test@example.com>
411
 
branch nick: parent
412
 
timestamp: Just now
413
 
message:
414
 
  merge branch 1
415
 
    ------------------------------------------------------------
416
 
    revno: 1.1.2
417
 
    committer: Lorem Ipsum <test@example.com>
418
 
    branch nick: child
419
 
    timestamp: Just now
420
 
    message:
421
 
      merge branch 2
422
 
        ------------------------------------------------------------
423
 
        revno: 1.2.1
424
 
        committer: Lorem Ipsum <test@example.com>
425
 
        branch nick: smallerchild
426
 
        timestamp: Just now
427
 
        message:
428
 
          branch 2
429
 
    ------------------------------------------------------------
430
 
    revno: 1.1.1
431
 
    committer: Lorem Ipsum <test@example.com>
432
 
    branch nick: child
433
 
    timestamp: Just now
434
 
    message:
435
 
      branch 1
436
 
------------------------------------------------------------
437
 
revno: 1
438
 
committer: Lorem Ipsum <test@example.com>
439
 
branch nick: parent
440
 
timestamp: Just now
441
 
message:
442
 
  first post
443
 
""",
444
 
                             the_log)
445
 
 
446
 
    def test_verbose_merge_revisions_contain_deltas(self):
447
 
        wt = self.make_branch_and_tree('parent')
448
 
        self.build_tree(['parent/f1', 'parent/f2'])
449
 
        wt.add(['f1','f2'])
450
 
        wt.commit('first post')
451
 
        self.run_bzr('branch parent child')
452
 
        os.unlink('child/f1')
453
 
        file('child/f2', 'wb').write('hello\n')
454
 
        self.run_bzr(['commit', '-m', 'removed f1 and modified f2',
455
 
            'child'])
456
 
        os.chdir('parent')
457
 
        self.run_bzr('merge ../child')
458
 
        wt.commit('merge branch 1')
459
 
        b = wt.branch
460
 
        sio = self.make_utf8_encoded_stringio()
461
 
        lf = log.LongLogFormatter(to_file=sio)
462
 
        log.show_log(b, lf, verbose=True)
463
 
        the_log = normalize_log(sio.getvalue())
464
 
        self.assertEqualDiff("""\
465
 
------------------------------------------------------------
466
 
revno: 2
467
 
committer: Lorem Ipsum <test@example.com>
468
 
branch nick: parent
469
 
timestamp: Just now
470
 
message:
471
 
  merge branch 1
472
 
removed:
473
 
  f1
474
 
modified:
475
 
  f2
476
 
    ------------------------------------------------------------
477
 
    revno: 1.1.1
478
 
    committer: Lorem Ipsum <test@example.com>
479
 
    branch nick: child
480
 
    timestamp: Just now
481
 
    message:
482
 
      removed f1 and modified f2
483
 
    removed:
484
 
      f1
485
 
    modified:
486
 
      f2
487
 
------------------------------------------------------------
488
 
revno: 1
489
 
committer: Lorem Ipsum <test@example.com>
490
 
branch nick: parent
491
 
timestamp: Just now
492
 
message:
493
 
  first post
494
 
added:
495
 
  f1
496
 
  f2
497
 
""",
498
 
                             the_log)
499
 
 
500
 
    def test_trailing_newlines(self):
501
 
        wt = self.make_branch_and_tree('.')
502
 
        b = make_commits_with_trailing_newlines(wt)
503
 
        sio = self.make_utf8_encoded_stringio()
504
 
        lf = log.LongLogFormatter(to_file=sio)
505
 
        log.show_log(b, lf)
506
 
        self.assertEqualDiff("""\
 
216
""")
 
217
 
 
218
        sio = StringIO()
 
219
        lf = LongLogFormatter(to_file=sio)
 
220
        show_log(b, lf)
 
221
        self.assertEquals(sio.getvalue(), """\
507
222
------------------------------------------------------------
508
223
revno: 3
509
224
committer: Joe Foo <joe@foo.com>
513
228
  single line with trailing newline
514
229
------------------------------------------------------------
515
230
revno: 2
516
 
author: Joe Bar <joe@bar.com>
517
231
committer: Joe Foo <joe@foo.com>
518
232
branch nick: test
519
233
timestamp: Mon 2005-11-21 09:27:22 -0600
528
242
timestamp: Mon 2005-11-21 09:24:15 -0600
529
243
message:
530
244
  simple log message
531
 
""",
532
 
                             sio.getvalue())
533
 
 
534
 
    def test_author_in_log(self):
535
 
        """Log includes the author name if it's set in
536
 
        the revision properties
537
 
        """
538
 
        wt = self.make_branch_and_tree('.')
539
 
        b = wt.branch
540
 
        self.build_tree(['a'])
541
 
        wt.add('a')
542
 
        b.nick = 'test_author_log'
543
 
        wt.commit(message='add a',
544
 
                  timestamp=1132711707,
545
 
                  timezone=36000,
546
 
                  committer='Lorem Ipsum <test@example.com>',
547
 
                  author='John Doe <jdoe@example.com>')
548
 
        sio = StringIO()
549
 
        formatter = log.LongLogFormatter(to_file=sio)
550
 
        log.show_log(b, formatter)
551
 
        self.assertEqualDiff('''\
552
 
------------------------------------------------------------
553
 
revno: 1
554
 
author: John Doe <jdoe@example.com>
555
 
committer: Lorem Ipsum <test@example.com>
556
 
branch nick: test_author_log
557
 
timestamp: Wed 2005-11-23 12:08:27 +1000
558
 
message:
559
 
  add a
560
 
''',
561
 
                             sio.getvalue())
562
 
 
563
 
    def test_properties_in_log(self):
564
 
        """Log includes the custom properties returned by the registered 
565
 
        handlers.
566
 
        """
567
 
        wt = self.make_branch_and_tree('.')
568
 
        b = wt.branch
569
 
        self.build_tree(['a'])
570
 
        wt.add('a')
571
 
        b.nick = 'test_properties_in_log'
572
 
        wt.commit(message='add a',
573
 
                  timestamp=1132711707,
574
 
                  timezone=36000,
575
 
                  committer='Lorem Ipsum <test@example.com>',
576
 
                  author='John Doe <jdoe@example.com>')
577
 
        sio = StringIO()
578
 
        formatter = log.LongLogFormatter(to_file=sio)
579
 
        try:
580
 
            def trivial_custom_prop_handler(revision):
581
 
                return {'test_prop':'test_value'}
582
 
 
583
 
            log.properties_handler_registry.register(
584
 
                'trivial_custom_prop_handler',
585
 
                trivial_custom_prop_handler)
586
 
            log.show_log(b, formatter)
587
 
        finally:
588
 
            log.properties_handler_registry.remove(
589
 
                'trivial_custom_prop_handler')
590
 
            self.assertEqualDiff('''\
591
 
------------------------------------------------------------
592
 
revno: 1
593
 
test_prop: test_value
594
 
author: John Doe <jdoe@example.com>
595
 
committer: Lorem Ipsum <test@example.com>
596
 
branch nick: test_properties_in_log
597
 
timestamp: Wed 2005-11-23 12:08:27 +1000
598
 
message:
599
 
  add a
600
 
''',
601
 
                                 sio.getvalue())
602
 
 
603
 
    def test_error_in_properties_handler(self):
604
 
        """Log includes the custom properties returned by the registered 
605
 
        handlers.
606
 
        """
607
 
        wt = self.make_branch_and_tree('.')
608
 
        b = wt.branch
609
 
        self.build_tree(['a'])
610
 
        wt.add('a')
611
 
        b.nick = 'test_author_log'
612
 
        wt.commit(message='add a',
613
 
                  timestamp=1132711707,
614
 
                  timezone=36000,
615
 
                  committer='Lorem Ipsum <test@example.com>',
616
 
                  author='John Doe <jdoe@example.com>',
617
 
                  revprops={'first_prop':'first_value'})
618
 
        sio = StringIO()
619
 
        formatter = log.LongLogFormatter(to_file=sio)
620
 
        try:
621
 
            def trivial_custom_prop_handler(revision):
622
 
                raise StandardError("a test error")
623
 
 
624
 
            log.properties_handler_registry.register(
625
 
                'trivial_custom_prop_handler',
626
 
                trivial_custom_prop_handler)
627
 
            self.assertRaises(StandardError, log.show_log, b, formatter,)
628
 
        finally:
629
 
            log.properties_handler_registry.remove(
630
 
                'trivial_custom_prop_handler')
631
 
 
632
 
    def test_properties_handler_bad_argument(self):
633
 
        wt = self.make_branch_and_tree('.')
634
 
        b = wt.branch
635
 
        self.build_tree(['a'])
636
 
        wt.add('a')
637
 
        b.nick = 'test_author_log'
638
 
        wt.commit(message='add a',
639
 
                  timestamp=1132711707,
640
 
                  timezone=36000,
641
 
                  committer='Lorem Ipsum <test@example.com>',
642
 
                  author='John Doe <jdoe@example.com>',
643
 
                  revprops={'a_prop':'test_value'})
644
 
        sio = StringIO()
645
 
        formatter = log.LongLogFormatter(to_file=sio)
646
 
        try:
647
 
            def bad_argument_prop_handler(revision):
648
 
                return {'custom_prop_name':revision.properties['a_prop']}
649
 
 
650
 
            log.properties_handler_registry.register(
651
 
                'bad_argument_prop_handler',
652
 
                bad_argument_prop_handler)
653
 
 
654
 
            self.assertRaises(AttributeError, formatter.show_properties,
655
 
                              'a revision', '')
656
 
 
657
 
            revision = b.repository.get_revision(b.last_revision())
658
 
            formatter.show_properties(revision, '')
659
 
            self.assertEqualDiff('''custom_prop_name: test_value\n''',
660
 
                                 sio.getvalue())
661
 
        finally:
662
 
            log.properties_handler_registry.remove(
663
 
                'bad_argument_prop_handler')
664
 
 
665
 
 
666
 
class TestLineLogFormatter(tests.TestCaseWithTransport):
667
 
 
668
 
    def test_line_log(self):
669
 
        """Line log should show revno
670
 
        
671
 
        bug #5162
672
 
        """
673
 
        wt = self.make_branch_and_tree('.')
674
 
        b = wt.branch
675
 
        self.build_tree(['a'])
676
 
        wt.add('a')
677
 
        b.nick = 'test-line-log'
678
 
        wt.commit(message='add a',
679
 
                  timestamp=1132711707,
680
 
                  timezone=36000,
681
 
                  committer='Line-Log-Formatter Tester <test@line.log>')
 
245
""")
 
246
        
 
247
    def test_verbose_log(self):
 
248
        """Verbose log includes changed files
 
249
        
 
250
        bug #4676
 
251
        """
 
252
        wt = self.make_branch_and_tree('.')
 
253
        b = wt.branch
 
254
        self.build_tree(['a'])
 
255
        wt.add('a')
 
256
        # XXX: why does a longer nick show up?
 
257
        b.nick = 'test_verbose_log'
 
258
        wt.commit(message='add a', 
 
259
                  timestamp=1132711707, 
 
260
                  timezone=36000,
 
261
                  committer='Lorem Ipsum <test@example.com>')
682
262
        logfile = file('out.tmp', 'w+')
683
 
        formatter = log.LineLogFormatter(to_file=logfile)
684
 
        log.show_log(b, formatter)
 
263
        formatter = LongLogFormatter(to_file=logfile)
 
264
        show_log(b, formatter, verbose=True)
685
265
        logfile.flush()
686
266
        logfile.seek(0)
687
267
        log_contents = logfile.read()
688
 
        self.assertEqualDiff('1: Line-Log-Formatte... 2005-11-23 add a\n',
689
 
                             log_contents)
690
 
 
691
 
    def test_trailing_newlines(self):
692
 
        wt = self.make_branch_and_tree('.')
693
 
        b = make_commits_with_trailing_newlines(wt)
694
 
        sio = self.make_utf8_encoded_stringio()
695
 
        lf = log.LineLogFormatter(to_file=sio)
696
 
        log.show_log(b, lf)
697
 
        self.assertEqualDiff("""\
698
 
3: Joe Foo 2005-11-21 single line with trailing newline
699
 
2: Joe Bar 2005-11-21 multiline
700
 
1: Joe Foo 2005-11-21 simple log message
701
 
""",
702
 
                             sio.getvalue())
703
 
 
704
 
    def test_line_log_single_merge_revision(self):
705
 
        wt = self.make_branch_and_memory_tree('.')
706
 
        wt.lock_write()
707
 
        self.addCleanup(wt.unlock)
708
 
        wt.add('')
709
 
        wt.commit('rev-1', rev_id='rev-1',
710
 
                  timestamp=1132586655, timezone=36000,
711
 
                  committer='Joe Foo <joe@foo.com>')
712
 
        wt.commit('rev-merged', rev_id='rev-2a',
713
 
                  timestamp=1132586700, timezone=36000,
714
 
                  committer='Joe Foo <joe@foo.com>')
715
 
        wt.set_parent_ids(['rev-1', 'rev-2a'])
716
 
        wt.branch.set_last_revision_info(1, 'rev-1')
717
 
        wt.commit('rev-2', rev_id='rev-2b',
718
 
                  timestamp=1132586800, timezone=36000,
719
 
                  committer='Joe Foo <joe@foo.com>')
720
 
        logfile = self.make_utf8_encoded_stringio()
721
 
        formatter = log.LineLogFormatter(to_file=logfile)
722
 
        revspec = revisionspec.RevisionSpec.from_string('1.1.1')
723
 
        wtb = wt.branch
724
 
        rev = revspec.in_history(wtb)
725
 
        log.show_log(wtb, formatter, start_revision=rev, end_revision=rev)
726
 
        self.assertEqualDiff("""\
727
 
1.1.1: Joe Foo 2005-11-22 rev-merged
728
 
""",
729
 
                             logfile.getvalue())
730
 
 
731
 
 
732
 
 
733
 
class TestGetViewRevisions(tests.TestCaseWithTransport):
734
 
 
735
 
    def make_tree_with_commits(self):
736
 
        """Create a tree with well-known revision ids"""
737
 
        wt = self.make_branch_and_tree('tree1')
738
 
        wt.commit('commit one', rev_id='1')
739
 
        wt.commit('commit two', rev_id='2')
740
 
        wt.commit('commit three', rev_id='3')
741
 
        mainline_revs = [None, '1', '2', '3']
742
 
        rev_nos = {'1': 1, '2': 2, '3': 3}
743
 
        return mainline_revs, rev_nos, wt
744
 
 
745
 
    def make_tree_with_merges(self):
746
 
        """Create a tree with well-known revision ids and a merge"""
747
 
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
748
 
        tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
749
 
        tree2.commit('four-a', rev_id='4a')
750
 
        wt.merge_from_branch(tree2.branch)
751
 
        wt.commit('four-b', rev_id='4b')
752
 
        mainline_revs.append('4b')
753
 
        rev_nos['4b'] = 4
754
 
        # 4a: 3.1.1
755
 
        return mainline_revs, rev_nos, wt
756
 
 
757
 
    def make_tree_with_many_merges(self):
758
 
        """Create a tree with well-known revision ids"""
759
 
        wt = self.make_branch_and_tree('tree1')
760
 
        self.build_tree_contents([('tree1/f', '1\n')])
761
 
        wt.add(['f'], ['f-id'])
762
 
        wt.commit('commit one', rev_id='1')
763
 
        wt.commit('commit two', rev_id='2')
764
 
 
765
 
        tree3 = wt.bzrdir.sprout('tree3').open_workingtree()
766
 
        self.build_tree_contents([('tree3/f', '1\n2\n3a\n')])
767
 
        tree3.commit('commit three a', rev_id='3a')
768
 
 
769
 
        tree2 = wt.bzrdir.sprout('tree2').open_workingtree()
770
 
        tree2.merge_from_branch(tree3.branch)
771
 
        tree2.commit('commit three b', rev_id='3b')
772
 
 
773
 
        wt.merge_from_branch(tree2.branch)
774
 
        wt.commit('commit three c', rev_id='3c')
775
 
        tree2.commit('four-a', rev_id='4a')
776
 
 
777
 
        wt.merge_from_branch(tree2.branch)
778
 
        wt.commit('four-b', rev_id='4b')
779
 
 
780
 
        mainline_revs = [None, '1', '2', '3c', '4b']
781
 
        rev_nos = {'1':1, '2':2, '3c': 3, '4b':4}
782
 
        full_rev_nos_for_reference = {
783
 
            '1': '1',
784
 
            '2': '2',
785
 
            '3a': '2.1.1', #first commit tree 3
786
 
            '3b': '2.2.1', # first commit tree 2
787
 
            '3c': '3', #merges 3b to main
788
 
            '4a': '2.2.2', # second commit tree 2
789
 
            '4b': '4', # merges 4a to main
790
 
            }
791
 
        return mainline_revs, rev_nos, wt
792
 
 
793
 
    def test_get_view_revisions_forward(self):
794
 
        """Test the get_view_revisions method"""
795
 
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
796
 
        wt.lock_read()
797
 
        self.addCleanup(wt.unlock)
798
 
        revisions = list(log.get_view_revisions(
799
 
                mainline_revs, rev_nos, wt.branch, 'forward'))
800
 
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0)],
801
 
                         revisions)
802
 
        revisions2 = list(log.get_view_revisions(
803
 
                mainline_revs, rev_nos, wt.branch, 'forward',
804
 
                include_merges=False))
805
 
        self.assertEqual(revisions, revisions2)
806
 
 
807
 
    def test_get_view_revisions_reverse(self):
808
 
        """Test the get_view_revisions with reverse"""
809
 
        mainline_revs, rev_nos, wt = self.make_tree_with_commits()
810
 
        wt.lock_read()
811
 
        self.addCleanup(wt.unlock)
812
 
        revisions = list(log.get_view_revisions(
813
 
                mainline_revs, rev_nos, wt.branch, 'reverse'))
814
 
        self.assertEqual([('3', '3', 0), ('2', '2', 0), ('1', '1', 0), ],
815
 
                         revisions)
816
 
        revisions2 = list(log.get_view_revisions(
817
 
                mainline_revs, rev_nos, wt.branch, 'reverse',
818
 
                include_merges=False))
819
 
        self.assertEqual(revisions, revisions2)
820
 
 
821
 
    def test_get_view_revisions_merge(self):
822
 
        """Test get_view_revisions when there are merges"""
823
 
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
824
 
        wt.lock_read()
825
 
        self.addCleanup(wt.unlock)
826
 
        revisions = list(log.get_view_revisions(
827
 
                mainline_revs, rev_nos, wt.branch, 'forward'))
828
 
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
829
 
                          ('4b', '4', 0), ('4a', '3.1.1', 1)],
830
 
                         revisions)
831
 
        revisions = list(log.get_view_revisions(
832
 
                mainline_revs, rev_nos, wt.branch, 'forward',
833
 
                include_merges=False))
834
 
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3', '3', 0),
835
 
                          ('4b', '4', 0)],
836
 
                         revisions)
837
 
 
838
 
    def test_get_view_revisions_merge_reverse(self):
839
 
        """Test get_view_revisions in reverse when there are merges"""
840
 
        mainline_revs, rev_nos, wt = self.make_tree_with_merges()
841
 
        wt.lock_read()
842
 
        self.addCleanup(wt.unlock)
843
 
        revisions = list(log.get_view_revisions(
844
 
                mainline_revs, rev_nos, wt.branch, 'reverse'))
845
 
        self.assertEqual([('4b', '4', 0), ('4a', '3.1.1', 1),
846
 
                          ('3', '3', 0), ('2', '2', 0), ('1', '1', 0)],
847
 
                         revisions)
848
 
        revisions = list(log.get_view_revisions(
849
 
                mainline_revs, rev_nos, wt.branch, 'reverse',
850
 
                include_merges=False))
851
 
        self.assertEqual([('4b', '4', 0), ('3', '3', 0), ('2', '2', 0),
852
 
                          ('1', '1', 0)],
853
 
                         revisions)
854
 
 
855
 
    def test_get_view_revisions_merge2(self):
856
 
        """Test get_view_revisions when there are merges"""
857
 
        mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
858
 
        wt.lock_read()
859
 
        self.addCleanup(wt.unlock)
860
 
        revisions = list(log.get_view_revisions(
861
 
                mainline_revs, rev_nos, wt.branch, 'forward'))
862
 
        expected = [('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
863
 
                    ('3a', '2.1.1', 1), ('3b', '2.2.1', 1), ('4b', '4', 0),
864
 
                    ('4a', '2.2.2', 1)]
865
 
        self.assertEqual(expected, revisions)
866
 
        revisions = list(log.get_view_revisions(
867
 
                mainline_revs, rev_nos, wt.branch, 'forward',
868
 
                include_merges=False))
869
 
        self.assertEqual([('1', '1', 0), ('2', '2', 0), ('3c', '3', 0),
870
 
                          ('4b', '4', 0)],
871
 
                         revisions)
872
 
 
873
 
 
874
 
    def test_file_id_for_range(self):
875
 
        mainline_revs, rev_nos, wt = self.make_tree_with_many_merges()
876
 
        wt.lock_read()
877
 
        self.addCleanup(wt.unlock)
878
 
 
879
 
        def rev_from_rev_id(revid, branch):
880
 
            revspec = revisionspec.RevisionSpec.from_string('revid:%s' % revid)
881
 
            return revspec.in_history(branch)
882
 
 
883
 
        def view_revs(start_rev, end_rev, file_id, direction):
884
 
            revs = log.calculate_view_revisions(
885
 
                wt.branch,
886
 
                start_rev, # start_revision
887
 
                end_rev, # end_revision
888
 
                direction, # direction
889
 
                file_id, # specific_fileid
890
 
                True, # generate_merge_revisions
891
 
                True, # allow_single_merge_revision
892
 
                )
893
 
            return revs
894
 
 
895
 
        rev_3a = rev_from_rev_id('3a', wt.branch)
896
 
        rev_4b = rev_from_rev_id('4b', wt.branch)
897
 
        self.assertEqual([('3c', '3', 0), ('3a', '2.1.1', 1)],
898
 
                          view_revs(rev_3a, rev_4b, 'f-id', 'reverse'))
899
 
        # Note that the depth is 0 for 3a because depths are normalized, but
900
 
        # there is still a bug somewhere... most probably in
901
 
        # _filter_revision_range and/or get_view_revisions still around a bad
902
 
        # use of reverse_by_depth
903
 
        self.assertEqual([('3a', '2.1.1', 0)],
904
 
                          view_revs(rev_3a, rev_4b, 'f-id', 'forward'))
905
 
 
906
 
 
907
 
class TestGetRevisionsTouchingFileID(tests.TestCaseWithTransport):
908
 
 
909
 
    def create_tree_with_single_merge(self):
910
 
        """Create a branch with a moderate layout.
911
 
 
912
 
        The revision graph looks like:
913
 
 
914
 
           A
915
 
           |\
916
 
           B C
917
 
           |/
918
 
           D
919
 
 
920
 
        In this graph, A introduced files f1 and f2 and f3.
921
 
        B modifies f1 and f3, and C modifies f2 and f3.
922
 
        D merges the changes from B and C and resolves the conflict for f3.
923
 
        """
924
 
        # TODO: jam 20070218 This seems like it could really be done
925
 
        #       with make_branch_and_memory_tree() if we could just
926
 
        #       create the content of those files.
927
 
        # TODO: jam 20070218 Another alternative is that we would really
928
 
        #       like to only create this tree 1 time for all tests that
929
 
        #       use it. Since 'log' only uses the tree in a readonly
930
 
        #       fashion, it seems a shame to regenerate an identical
931
 
        #       tree for each test.
932
 
        tree = self.make_branch_and_tree('tree')
933
 
        tree.lock_write()
934
 
        self.addCleanup(tree.unlock)
935
 
 
936
 
        self.build_tree_contents([('tree/f1', 'A\n'),
937
 
                                  ('tree/f2', 'A\n'),
938
 
                                  ('tree/f3', 'A\n'),
939
 
                                 ])
940
 
        tree.add(['f1', 'f2', 'f3'], ['f1-id', 'f2-id', 'f3-id'])
941
 
        tree.commit('A', rev_id='A')
942
 
 
943
 
        self.build_tree_contents([('tree/f2', 'A\nC\n'),
944
 
                                  ('tree/f3', 'A\nC\n'),
945
 
                                 ])
946
 
        tree.commit('C', rev_id='C')
947
 
        # Revert back to A to build the other history.
948
 
        tree.set_last_revision('A')
949
 
        tree.branch.set_last_revision_info(1, 'A')
950
 
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
951
 
                                  ('tree/f2', 'A\n'),
952
 
                                  ('tree/f3', 'A\nB\n'),
953
 
                                 ])
954
 
        tree.commit('B', rev_id='B')
955
 
        tree.set_parent_ids(['B', 'C'])
956
 
        self.build_tree_contents([('tree/f1', 'A\nB\n'),
957
 
                                  ('tree/f2', 'A\nC\n'),
958
 
                                  ('tree/f3', 'A\nB\nC\n'),
959
 
                                 ])
960
 
        tree.commit('D', rev_id='D')
961
 
 
962
 
        # Switch to a read lock for this tree.
963
 
        # We still have an addCleanup(tree.unlock) pending
964
 
        tree.unlock()
965
 
        tree.lock_read()
966
 
        return tree
967
 
 
968
 
    def check_delta(self, delta, **kw):
969
 
        """Check the filenames touched by a delta are as expected.
970
 
 
971
 
        Caller only have to pass in the list of files for each part, all
972
 
        unspecified parts are considered empty (and checked as such).
973
 
        """
974
 
        for n in 'added', 'removed', 'renamed', 'modified', 'unchanged':
975
 
            # By default we expect an empty list
976
 
            expected = kw.get(n, [])
977
 
            # strip out only the path components
978
 
            got = [x[0] for x in getattr(delta, n)]
979
 
            self.assertEqual(expected, got)
980
 
 
981
 
    def test_tree_with_single_merge(self):
982
 
        """Make sure the tree layout is correct."""
983
 
        tree = self.create_tree_with_single_merge()
984
 
        rev_A_tree = tree.branch.repository.revision_tree('A')
985
 
        rev_B_tree = tree.branch.repository.revision_tree('B')
986
 
        rev_C_tree = tree.branch.repository.revision_tree('C')
987
 
        rev_D_tree = tree.branch.repository.revision_tree('D')
988
 
 
989
 
        self.check_delta(rev_B_tree.changes_from(rev_A_tree),
990
 
                         modified=['f1', 'f3'])
991
 
 
992
 
        self.check_delta(rev_C_tree.changes_from(rev_A_tree),
993
 
                         modified=['f2', 'f3'])
994
 
 
995
 
        self.check_delta(rev_D_tree.changes_from(rev_B_tree),
996
 
                         modified=['f2', 'f3'])
997
 
 
998
 
        self.check_delta(rev_D_tree.changes_from(rev_C_tree),
999
 
                         modified=['f1', 'f3'])
1000
 
 
1001
 
    def assertAllRevisionsForFileID(self, tree, file_id, revisions):
1002
 
        """Ensure _filter_revisions_touching_file_id returns the right values.
1003
 
 
1004
 
        Get the return value from _filter_revisions_touching_file_id and make
1005
 
        sure they are correct.
1006
 
        """
1007
 
        # The api for _filter_revisions_touching_file_id is a little crazy.
1008
 
        # So we do the setup here.
1009
 
        mainline = tree.branch.revision_history()
1010
 
        mainline.insert(0, None)
1011
 
        revnos = dict((rev, idx+1) for idx, rev in enumerate(mainline))
1012
 
        view_revs_iter = log.get_view_revisions(mainline, revnos, tree.branch,
1013
 
                                                'reverse', True)
1014
 
        actual_revs = log._filter_revisions_touching_file_id(
1015
 
                            tree.branch,
1016
 
                            file_id,
1017
 
                            list(view_revs_iter))
1018
 
        self.assertEqual(revisions, [r for r, revno, depth in actual_revs])
1019
 
 
1020
 
    def test_file_id_f1(self):
1021
 
        tree = self.create_tree_with_single_merge()
1022
 
        # f1 should be marked as modified by revisions A and B
1023
 
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['B', 'A'])
1024
 
 
1025
 
    def test_file_id_f2(self):
1026
 
        tree = self.create_tree_with_single_merge()
1027
 
        # f2 should be marked as modified by revisions A, C, and D
1028
 
        # because D merged the changes from C.
1029
 
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1030
 
 
1031
 
    def test_file_id_f3(self):
1032
 
        tree = self.create_tree_with_single_merge()
1033
 
        # f3 should be marked as modified by revisions A, B, C, and D
1034
 
        self.assertAllRevisionsForFileID(tree, 'f3-id', ['D', 'C', 'B', 'A'])
1035
 
 
1036
 
    def test_file_id_with_ghosts(self):
1037
 
        # This is testing bug #209948, where having a ghost would cause
1038
 
        # _filter_revisions_touching_file_id() to fail.
1039
 
        tree = self.create_tree_with_single_merge()
1040
 
        # We need to add a revision, so switch back to a write-locked tree
1041
 
        # (still a single addCleanup(tree.unlock) pending).
1042
 
        tree.unlock()
1043
 
        tree.lock_write()
1044
 
        first_parent = tree.last_revision()
1045
 
        tree.set_parent_ids([first_parent, 'ghost-revision-id'])
1046
 
        self.build_tree_contents([('tree/f1', 'A\nB\nXX\n')])
1047
 
        tree.commit('commit with a ghost', rev_id='XX')
1048
 
        self.assertAllRevisionsForFileID(tree, 'f1-id', ['XX', 'B', 'A'])
1049
 
        self.assertAllRevisionsForFileID(tree, 'f2-id', ['D', 'C', 'A'])
1050
 
 
1051
 
 
1052
 
class TestShowChangedRevisions(tests.TestCaseWithTransport):
1053
 
 
1054
 
    def test_show_changed_revisions_verbose(self):
1055
 
        tree = self.make_branch_and_tree('tree_a')
1056
 
        self.build_tree(['tree_a/foo'])
1057
 
        tree.add('foo')
1058
 
        tree.commit('bar', rev_id='bar-id')
1059
 
        s = self.make_utf8_encoded_stringio()
1060
 
        log.show_changed_revisions(tree.branch, [], ['bar-id'], s)
1061
 
        self.assertContainsRe(s.getvalue(), 'bar')
1062
 
        self.assertNotContainsRe(s.getvalue(), 'foo')
1063
 
 
1064
 
 
1065
 
class TestLogFormatter(tests.TestCase):
1066
 
 
1067
 
    def test_short_committer(self):
1068
 
        rev = revision.Revision('a-id')
1069
 
        rev.committer = 'John Doe <jdoe@example.com>'
1070
 
        lf = log.LogFormatter(None)
1071
 
        self.assertEqual('John Doe', lf.short_committer(rev))
1072
 
        rev.committer = 'John Smith <jsmith@example.com>'
1073
 
        self.assertEqual('John Smith', lf.short_committer(rev))
1074
 
        rev.committer = 'John Smith'
1075
 
        self.assertEqual('John Smith', lf.short_committer(rev))
1076
 
        rev.committer = 'jsmith@example.com'
1077
 
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1078
 
        rev.committer = '<jsmith@example.com>'
1079
 
        self.assertEqual('jsmith@example.com', lf.short_committer(rev))
1080
 
        rev.committer = 'John Smith jsmith@example.com'
1081
 
        self.assertEqual('John Smith', lf.short_committer(rev))
1082
 
 
1083
 
    def test_short_author(self):
1084
 
        rev = revision.Revision('a-id')
1085
 
        rev.committer = 'John Doe <jdoe@example.com>'
1086
 
        lf = log.LogFormatter(None)
1087
 
        self.assertEqual('John Doe', lf.short_author(rev))
1088
 
        rev.properties['author'] = 'John Smith <jsmith@example.com>'
1089
 
        self.assertEqual('John Smith', lf.short_author(rev))
1090
 
        rev.properties['author'] = 'John Smith'
1091
 
        self.assertEqual('John Smith', lf.short_author(rev))
1092
 
        rev.properties['author'] = 'jsmith@example.com'
1093
 
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
1094
 
        rev.properties['author'] = '<jsmith@example.com>'
1095
 
        self.assertEqual('jsmith@example.com', lf.short_author(rev))
1096
 
        rev.properties['author'] = 'John Smith jsmith@example.com'
1097
 
        self.assertEqual('John Smith', lf.short_author(rev))
1098
 
 
1099
 
 
1100
 
class TestReverseByDepth(tests.TestCase):
1101
 
    """Test reverse_by_depth behavior.
1102
 
 
1103
 
    This is used to present revisions in forward (oldest first) order in a nice
1104
 
    layout.
1105
 
 
1106
 
    The tests use lighter revision description to ease reading.
1107
 
    """
1108
 
 
1109
 
    def assertReversed(self, forward, backward):
1110
 
        # Transform the descriptions to suit the API: tests use (revno, depth),
1111
 
        # while the API expects (revid, revno, depth)
1112
 
        def complete_revisions(l):
1113
 
            """Transform the description to suit the API.
1114
 
 
1115
 
            Tests use (revno, depth) whil the API expects (revid, revno, depth).
1116
 
            Since the revid is arbitrary, we just duplicate revno
1117
 
            """
1118
 
            return [ (r, r, d) for r, d in l]
1119
 
        forward = complete_revisions(forward)
1120
 
        backward= complete_revisions(backward)
1121
 
        self.assertEqual(forward, log.reverse_by_depth(backward))
1122
 
 
1123
 
 
1124
 
    def test_mainline_revisions(self):
1125
 
        self.assertReversed([( '1', 0), ('2', 0)],
1126
 
                            [('2', 0), ('1', 0)])
1127
 
 
1128
 
    def test_merged_revisions(self):
1129
 
        self.assertReversed([('1', 0), ('2', 0), ('2.2', 1), ('2.1', 1),],
1130
 
                            [('2', 0), ('2.1', 1), ('2.2', 1), ('1', 0),])
1131
 
    def test_shifted_merged_revisions(self):
1132
 
        """Test irregular layout.
1133
 
 
1134
 
        Requesting revisions touching a file can produce "holes" in the depths.
1135
 
        """
1136
 
        self.assertReversed([('1', 0), ('2', 0), ('1.1', 2), ('1.2', 2),],
1137
 
                            [('2', 0), ('1.2', 2), ('1.1', 2), ('1', 0),])
1138
 
 
1139
 
    def test_merged_without_child_revisions(self):
1140
 
        """Test irregular layout.
1141
 
 
1142
 
        Revision ranges can produce "holes" in the depths.
1143
 
        """
1144
 
        # When a revision of higher depth doesn't follow one of lower depth, we
1145
 
        # assume a lower depth one is virtually there
1146
 
        self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1147
 
                            [('4', 4), ('3', 3), ('2', 2), ('1', 2),])
1148
 
        # So we get the same order after reversing below even if the original
1149
 
        # revisions are not in the same order.
1150
 
        self.assertReversed([('1', 2), ('2', 2), ('3', 3), ('4', 4)],
1151
 
                            [('3', 3), ('4', 4), ('2', 2), ('1', 2),])
 
268
        self.assertEqualDiff(log_contents, '''\
 
269
------------------------------------------------------------
 
270
revno: 1
 
271
committer: Lorem Ipsum <test@example.com>
 
272
branch nick: test_verbose_log
 
273
timestamp: Wed 2005-11-23 12:08:27 +1000
 
274
message:
 
275
  add a
 
276
added:
 
277
  a
 
278
''')