/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/tests/test_diff.py

  • Committer: Jelmer Vernooij
  • Date: 2020-05-24 00:39:50 UTC
  • mto: This revision was merged to the branch mainline in revision 7504.
  • Revision ID: jelmer@jelmer.uk-20200524003950-bbc545r76vc5yajg
Add github action.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2012, 2014, 2016, 2017 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
import contextlib
 
18
from io import BytesIO
 
19
import os
 
20
import re
 
21
import subprocess
 
22
import sys
 
23
import tempfile
 
24
 
 
25
from .. import (
 
26
    diff,
 
27
    errors,
 
28
    osutils,
 
29
    revision as _mod_revision,
 
30
    revisionspec,
 
31
    revisiontree,
 
32
    tests,
 
33
    )
 
34
from ..tests import (
 
35
    features,
 
36
    EncodingAdapter,
 
37
    )
 
38
from ..tests.scenarios import load_tests_apply_scenarios
 
39
 
 
40
 
 
41
load_tests = load_tests_apply_scenarios
 
42
 
 
43
 
 
44
def subst_dates(string):
 
45
    """Replace date strings with constant values."""
 
46
    return re.sub(br'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [-\+]\d{4}',
 
47
                  b'YYYY-MM-DD HH:MM:SS +ZZZZ', string)
 
48
 
 
49
 
 
50
def udiff_lines(old, new, allow_binary=False):
 
51
    output = BytesIO()
 
52
    diff.internal_diff('old', old, 'new', new, output, allow_binary)
 
53
    output.seek(0, 0)
 
54
    return output.readlines()
 
55
 
 
56
 
 
57
def external_udiff_lines(old, new, use_stringio=False):
 
58
    if use_stringio:
 
59
        # BytesIO has no fileno, so it tests a different codepath
 
60
        output = BytesIO()
 
61
    else:
 
62
        output = tempfile.TemporaryFile()
 
63
    try:
 
64
        diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
 
65
    except errors.NoDiff:
 
66
        raise tests.TestSkipped('external "diff" not present to test')
 
67
    output.seek(0, 0)
 
68
    lines = output.readlines()
 
69
    output.close()
 
70
    return lines
 
71
 
 
72
 
 
73
class StubO(object):
 
74
    """Simple file-like object that allows writes with any type and records."""
 
75
 
 
76
    def __init__(self):
 
77
        self.write_record = []
 
78
 
 
79
    def write(self, data):
 
80
        self.write_record.append(data)
 
81
 
 
82
    def check_types(self, testcase, expected_type):
 
83
        testcase.assertFalse(
 
84
            any(not isinstance(o, expected_type) for o in self.write_record),
 
85
            "Not all writes of type %s: %r" % (
 
86
                expected_type.__name__, self.write_record))
 
87
 
 
88
 
 
89
class TestDiffOptions(tests.TestCase):
 
90
 
 
91
    def test_unified_added(self):
 
92
        """Check for default style '-u' only if no other style specified
 
93
        in 'diff-options'.
 
94
        """
 
95
        # Verify that style defaults to unified, id est '-u' appended
 
96
        # to option list, in the absence of an alternative style.
 
97
        self.assertEqual(['-a', '-u'], diff.default_style_unified(['-a']))
 
98
 
 
99
 
 
100
class TestDiffOptionsScenarios(tests.TestCase):
 
101
 
 
102
    scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
 
103
    style = None  # Set by load_tests_apply_scenarios from scenarios
 
104
 
 
105
    def test_unified_not_added(self):
 
106
        # Verify that for all valid style options, '-u' is not
 
107
        # appended to option list.
 
108
        ret_opts = diff.default_style_unified(diff_opts=["%s" % (self.style,)])
 
109
        self.assertEqual(["%s" % (self.style,)], ret_opts)
 
110
 
 
111
 
 
112
class TestDiff(tests.TestCase):
 
113
 
 
114
    def test_add_nl(self):
 
115
        """diff generates a valid diff for patches that add a newline"""
 
116
        lines = udiff_lines([b'boo'], [b'boo\n'])
 
117
        self.check_patch(lines)
 
118
        self.assertEqual(lines[4], b'\\ No newline at end of file\n')
 
119
        ## "expected no-nl, got %r" % lines[4]
 
120
 
 
121
    def test_add_nl_2(self):
 
122
        """diff generates a valid diff for patches that change last line and
 
123
        add a newline.
 
124
        """
 
125
        lines = udiff_lines([b'boo'], [b'goo\n'])
 
126
        self.check_patch(lines)
 
127
        self.assertEqual(lines[4], b'\\ No newline at end of file\n')
 
128
        ## "expected no-nl, got %r" % lines[4]
 
129
 
 
130
    def test_remove_nl(self):
 
131
        """diff generates a valid diff for patches that change last line and
 
132
        add a newline.
 
133
        """
 
134
        lines = udiff_lines([b'boo\n'], [b'boo'])
 
135
        self.check_patch(lines)
 
136
        self.assertEqual(lines[5], b'\\ No newline at end of file\n')
 
137
        ## "expected no-nl, got %r" % lines[5]
 
138
 
 
139
    def check_patch(self, lines):
 
140
        self.assertTrue(len(lines) > 1)
 
141
        ## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
 
142
        self.assertTrue(lines[0].startswith(b'---'))
 
143
        ## 'No orig line for patch:\n%s' % "".join(lines)
 
144
        self.assertTrue(lines[1].startswith(b'+++'))
 
145
        ## 'No mod line for patch:\n%s' % "".join(lines)
 
146
        self.assertTrue(len(lines) > 2)
 
147
        ## "No hunks for patch:\n%s" % "".join(lines)
 
148
        self.assertTrue(lines[2].startswith(b'@@'))
 
149
        ## "No hunk header for patch:\n%s" % "".join(lines)
 
150
        self.assertTrue(b'@@' in lines[2][2:])
 
151
        ## "Unterminated hunk header for patch:\n%s" % "".join(lines)
 
152
 
 
153
    def test_binary_lines(self):
 
154
        empty = []
 
155
        uni_lines = [1023 * b'a' + b'\x00']
 
156
        self.assertRaises(errors.BinaryFile, udiff_lines, uni_lines, empty)
 
157
        self.assertRaises(errors.BinaryFile, udiff_lines, empty, uni_lines)
 
158
        udiff_lines(uni_lines, empty, allow_binary=True)
 
159
        udiff_lines(empty, uni_lines, allow_binary=True)
 
160
 
 
161
    def test_external_diff(self):
 
162
        lines = external_udiff_lines([b'boo\n'], [b'goo\n'])
 
163
        self.check_patch(lines)
 
164
        self.assertEqual(b'\n', lines[-1])
 
165
 
 
166
    def test_external_diff_no_fileno(self):
 
167
        # Make sure that we can handle not having a fileno, even
 
168
        # if the diff is large
 
169
        lines = external_udiff_lines([b'boo\n'] * 10000,
 
170
                                     [b'goo\n'] * 10000,
 
171
                                     use_stringio=True)
 
172
        self.check_patch(lines)
 
173
 
 
174
    def test_external_diff_binary_lang_c(self):
 
175
        for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
 
176
            self.overrideEnv(lang, 'C')
 
177
        lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
 
178
        # Older versions of diffutils say "Binary files", newer
 
179
        # versions just say "Files".
 
180
        self.assertContainsRe(
 
181
            lines[0], b'(Binary f|F)iles old and new differ\n')
 
182
        self.assertEqual(lines[1:], [b'\n'])
 
183
 
 
184
    def test_no_external_diff(self):
 
185
        """Check that NoDiff is raised when diff is not available"""
 
186
        # Make sure no 'diff' command is available
 
187
        # XXX: Weird, using None instead of '' breaks the test -- vila 20101216
 
188
        self.overrideEnv('PATH', '')
 
189
        self.assertRaises(errors.NoDiff, diff.external_diff,
 
190
                          b'old', [b'boo\n'], b'new', [b'goo\n'],
 
191
                          BytesIO(), diff_opts=['-u'])
 
192
 
 
193
    def test_internal_diff_default(self):
 
194
        # Default internal diff encoding is utf8
 
195
        output = BytesIO()
 
196
        diff.internal_diff(u'old_\xb5', [b'old_text\n'],
 
197
                           u'new_\xe5', [b'new_text\n'], output)
 
198
        lines = output.getvalue().splitlines(True)
 
199
        self.check_patch(lines)
 
200
        self.assertEqual([b'--- old_\xc2\xb5\n',
 
201
                          b'+++ new_\xc3\xa5\n',
 
202
                          b'@@ -1,1 +1,1 @@\n',
 
203
                          b'-old_text\n',
 
204
                          b'+new_text\n',
 
205
                          b'\n',
 
206
                          ], lines)
 
207
 
 
208
    def test_internal_diff_utf8(self):
 
209
        output = BytesIO()
 
210
        diff.internal_diff(u'old_\xb5', [b'old_text\n'],
 
211
                           u'new_\xe5', [b'new_text\n'], output,
 
212
                           path_encoding='utf8')
 
213
        lines = output.getvalue().splitlines(True)
 
214
        self.check_patch(lines)
 
215
        self.assertEqual([b'--- old_\xc2\xb5\n',
 
216
                          b'+++ new_\xc3\xa5\n',
 
217
                          b'@@ -1,1 +1,1 @@\n',
 
218
                          b'-old_text\n',
 
219
                          b'+new_text\n',
 
220
                          b'\n',
 
221
                          ], lines)
 
222
 
 
223
    def test_internal_diff_iso_8859_1(self):
 
224
        output = BytesIO()
 
225
        diff.internal_diff(u'old_\xb5', [b'old_text\n'],
 
226
                           u'new_\xe5', [b'new_text\n'], output,
 
227
                           path_encoding='iso-8859-1')
 
228
        lines = output.getvalue().splitlines(True)
 
229
        self.check_patch(lines)
 
230
        self.assertEqual([b'--- old_\xb5\n',
 
231
                          b'+++ new_\xe5\n',
 
232
                          b'@@ -1,1 +1,1 @@\n',
 
233
                          b'-old_text\n',
 
234
                          b'+new_text\n',
 
235
                          b'\n',
 
236
                          ], lines)
 
237
 
 
238
    def test_internal_diff_no_content(self):
 
239
        output = BytesIO()
 
240
        diff.internal_diff(u'old', [], u'new', [], output)
 
241
        self.assertEqual(b'', output.getvalue())
 
242
 
 
243
    def test_internal_diff_no_changes(self):
 
244
        output = BytesIO()
 
245
        diff.internal_diff(u'old', [b'text\n', b'contents\n'],
 
246
                           u'new', [b'text\n', b'contents\n'],
 
247
                           output)
 
248
        self.assertEqual(b'', output.getvalue())
 
249
 
 
250
    def test_internal_diff_returns_bytes(self):
 
251
        output = StubO()
 
252
        diff.internal_diff(u'old_\xb5', [b'old_text\n'],
 
253
                           u'new_\xe5', [b'new_text\n'], output)
 
254
        output.check_types(self, bytes)
 
255
 
 
256
    def test_internal_diff_default_context(self):
 
257
        output = BytesIO()
 
258
        diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
 
259
                                   b'same_text\n', b'same_text\n', b'old_text\n'],
 
260
                           'new', [b'same_text\n', b'same_text\n', b'same_text\n',
 
261
                                   b'same_text\n', b'same_text\n', b'new_text\n'], output)
 
262
        lines = output.getvalue().splitlines(True)
 
263
        self.check_patch(lines)
 
264
        self.assertEqual([b'--- old\n',
 
265
                          b'+++ new\n',
 
266
                          b'@@ -3,4 +3,4 @@\n',
 
267
                          b' same_text\n',
 
268
                          b' same_text\n',
 
269
                          b' same_text\n',
 
270
                          b'-old_text\n',
 
271
                          b'+new_text\n',
 
272
                          b'\n',
 
273
                          ], lines)
 
274
 
 
275
    def test_internal_diff_no_context(self):
 
276
        output = BytesIO()
 
277
        diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
 
278
                                   b'same_text\n', b'same_text\n', b'old_text\n'],
 
279
                           'new', [b'same_text\n', b'same_text\n', b'same_text\n',
 
280
                                   b'same_text\n', b'same_text\n', b'new_text\n'], output,
 
281
                           context_lines=0)
 
282
        lines = output.getvalue().splitlines(True)
 
283
        self.check_patch(lines)
 
284
        self.assertEqual([b'--- old\n',
 
285
                          b'+++ new\n',
 
286
                          b'@@ -6,1 +6,1 @@\n',
 
287
                          b'-old_text\n',
 
288
                          b'+new_text\n',
 
289
                          b'\n',
 
290
                          ], lines)
 
291
 
 
292
    def test_internal_diff_more_context(self):
 
293
        output = BytesIO()
 
294
        diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
 
295
                                   b'same_text\n', b'same_text\n', b'old_text\n'],
 
296
                           'new', [b'same_text\n', b'same_text\n', b'same_text\n',
 
297
                                   b'same_text\n', b'same_text\n', b'new_text\n'], output,
 
298
                           context_lines=4)
 
299
        lines = output.getvalue().splitlines(True)
 
300
        self.check_patch(lines)
 
301
        self.assertEqual([b'--- old\n',
 
302
                          b'+++ new\n',
 
303
                          b'@@ -2,5 +2,5 @@\n',
 
304
                          b' same_text\n',
 
305
                          b' same_text\n',
 
306
                          b' same_text\n',
 
307
                          b' same_text\n',
 
308
                          b'-old_text\n',
 
309
                          b'+new_text\n',
 
310
                          b'\n',
 
311
                          ], lines)
 
312
 
 
313
 
 
314
class TestDiffFiles(tests.TestCaseInTempDir):
 
315
 
 
316
    def test_external_diff_binary(self):
 
317
        """The output when using external diff should use diff's i18n error"""
 
318
        for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
 
319
            self.overrideEnv(lang, 'C')
 
320
        # Make sure external_diff doesn't fail in the current LANG
 
321
        lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
 
322
 
 
323
        cmd = ['diff', '-u', '--binary', 'old', 'new']
 
324
        with open('old', 'wb') as f:
 
325
            f.write(b'\x00foobar\n')
 
326
        with open('new', 'wb') as f:
 
327
            f.write(b'foo\x00bar\n')
 
328
        pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
 
329
                                stdin=subprocess.PIPE)
 
330
        out, err = pipe.communicate()
 
331
        # We should output whatever diff tells us, plus a trailing newline
 
332
        self.assertEqual(out.splitlines(True) + [b'\n'], lines)
 
333
 
 
334
 
 
335
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
 
336
    output = BytesIO()
 
337
    if working_tree is not None:
 
338
        extra_trees = (working_tree,)
 
339
    else:
 
340
        extra_trees = ()
 
341
    diff.show_diff_trees(tree1, tree2, output,
 
342
                         specific_files=specific_files,
 
343
                         extra_trees=extra_trees, old_label='old/',
 
344
                         new_label='new/')
 
345
    return output.getvalue()
 
346
 
 
347
 
 
348
class TestDiffDates(tests.TestCaseWithTransport):
 
349
 
 
350
    def setUp(self):
 
351
        super(TestDiffDates, self).setUp()
 
352
        self.wt = self.make_branch_and_tree('.')
 
353
        self.b = self.wt.branch
 
354
        self.build_tree_contents([
 
355
            ('file1', b'file1 contents at rev 1\n'),
 
356
            ('file2', b'file2 contents at rev 1\n')
 
357
            ])
 
358
        self.wt.add(['file1', 'file2'])
 
359
        self.wt.commit(
 
360
            message='Revision 1',
 
361
            timestamp=1143849600,  # 2006-04-01 00:00:00 UTC
 
362
            timezone=0,
 
363
            rev_id=b'rev-1')
 
364
        self.build_tree_contents([('file1', b'file1 contents at rev 2\n')])
 
365
        self.wt.commit(
 
366
            message='Revision 2',
 
367
            timestamp=1143936000,  # 2006-04-02 00:00:00 UTC
 
368
            timezone=28800,
 
369
            rev_id=b'rev-2')
 
370
        self.build_tree_contents([('file2', b'file2 contents at rev 3\n')])
 
371
        self.wt.commit(
 
372
            message='Revision 3',
 
373
            timestamp=1144022400,  # 2006-04-03 00:00:00 UTC
 
374
            timezone=-3600,
 
375
            rev_id=b'rev-3')
 
376
        self.wt.remove(['file2'])
 
377
        self.wt.commit(
 
378
            message='Revision 4',
 
379
            timestamp=1144108800,  # 2006-04-04 00:00:00 UTC
 
380
            timezone=0,
 
381
            rev_id=b'rev-4')
 
382
        self.build_tree_contents([
 
383
            ('file1', b'file1 contents in working tree\n')
 
384
            ])
 
385
        # set the date stamps for files in the working tree to known values
 
386
        os.utime('file1', (1144195200, 1144195200))  # 2006-04-05 00:00:00 UTC
 
387
 
 
388
    def test_diff_rev_tree_working_tree(self):
 
389
        output = get_diff_as_string(self.wt.basis_tree(), self.wt)
 
390
        # note that the date for old/file1 is from rev 2 rather than from
 
391
        # the basis revision (rev 4)
 
392
        self.assertEqualDiff(output, b'''\
 
393
=== modified file 'file1'
 
394
--- old/file1\t2006-04-02 00:00:00 +0000
 
395
+++ new/file1\t2006-04-05 00:00:00 +0000
 
396
@@ -1,1 +1,1 @@
 
397
-file1 contents at rev 2
 
398
+file1 contents in working tree
 
399
 
 
400
''')
 
401
 
 
402
    def test_diff_rev_tree_rev_tree(self):
 
403
        tree1 = self.b.repository.revision_tree(b'rev-2')
 
404
        tree2 = self.b.repository.revision_tree(b'rev-3')
 
405
        output = get_diff_as_string(tree1, tree2)
 
406
        self.assertEqualDiff(output, b'''\
 
407
=== modified file 'file2'
 
408
--- old/file2\t2006-04-01 00:00:00 +0000
 
409
+++ new/file2\t2006-04-03 00:00:00 +0000
 
410
@@ -1,1 +1,1 @@
 
411
-file2 contents at rev 1
 
412
+file2 contents at rev 3
 
413
 
 
414
''')
 
415
 
 
416
    def test_diff_add_files(self):
 
417
        tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
 
418
        tree2 = self.b.repository.revision_tree(b'rev-1')
 
419
        output = get_diff_as_string(tree1, tree2)
 
420
        # the files have the epoch time stamp for the tree in which
 
421
        # they don't exist.
 
422
        self.assertEqualDiff(output, b'''\
 
423
=== added file 'file1'
 
424
--- old/file1\t1970-01-01 00:00:00 +0000
 
425
+++ new/file1\t2006-04-01 00:00:00 +0000
 
426
@@ -0,0 +1,1 @@
 
427
+file1 contents at rev 1
 
428
 
 
429
=== added file 'file2'
 
430
--- old/file2\t1970-01-01 00:00:00 +0000
 
431
+++ new/file2\t2006-04-01 00:00:00 +0000
 
432
@@ -0,0 +1,1 @@
 
433
+file2 contents at rev 1
 
434
 
 
435
''')
 
436
 
 
437
    def test_diff_remove_files(self):
 
438
        tree1 = self.b.repository.revision_tree(b'rev-3')
 
439
        tree2 = self.b.repository.revision_tree(b'rev-4')
 
440
        output = get_diff_as_string(tree1, tree2)
 
441
        # the file has the epoch time stamp for the tree in which
 
442
        # it doesn't exist.
 
443
        self.assertEqualDiff(output, b'''\
 
444
=== removed file 'file2'
 
445
--- old/file2\t2006-04-03 00:00:00 +0000
 
446
+++ new/file2\t1970-01-01 00:00:00 +0000
 
447
@@ -1,1 +0,0 @@
 
448
-file2 contents at rev 3
 
449
 
 
450
''')
 
451
 
 
452
    def test_show_diff_specified(self):
 
453
        """A working tree filename can be used to identify a file"""
 
454
        self.wt.rename_one('file1', 'file1b')
 
455
        old_tree = self.b.repository.revision_tree(b'rev-1')
 
456
        new_tree = self.b.repository.revision_tree(b'rev-4')
 
457
        out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
 
458
                                 working_tree=self.wt)
 
459
        self.assertContainsRe(out, b'file1\t')
 
460
 
 
461
    def test_recursive_diff(self):
 
462
        """Children of directories are matched"""
 
463
        os.mkdir('dir1')
 
464
        os.mkdir('dir2')
 
465
        self.wt.add(['dir1', 'dir2'])
 
466
        self.wt.rename_one('file1', 'dir1/file1')
 
467
        old_tree = self.b.repository.revision_tree(b'rev-1')
 
468
        new_tree = self.b.repository.revision_tree(b'rev-4')
 
469
        out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
 
470
                                 working_tree=self.wt)
 
471
        self.assertContainsRe(out, b'file1\t')
 
472
        out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
 
473
                                 working_tree=self.wt)
 
474
        self.assertNotContainsRe(out, b'file1\t')
 
475
 
 
476
 
 
477
class TestShowDiffTrees(tests.TestCaseWithTransport):
 
478
    """Direct tests for show_diff_trees"""
 
479
 
 
480
    def test_modified_file(self):
 
481
        """Test when a file is modified."""
 
482
        tree = self.make_branch_and_tree('tree')
 
483
        self.build_tree_contents([('tree/file', b'contents\n')])
 
484
        tree.add(['file'], [b'file-id'])
 
485
        tree.commit('one', rev_id=b'rev-1')
 
486
 
 
487
        self.build_tree_contents([('tree/file', b'new contents\n')])
 
488
        d = get_diff_as_string(tree.basis_tree(), tree)
 
489
        self.assertContainsRe(d, b"=== modified file 'file'\n")
 
490
        self.assertContainsRe(d, b'--- old/file\t')
 
491
        self.assertContainsRe(d, b'\\+\\+\\+ new/file\t')
 
492
        self.assertContainsRe(d, b'-contents\n'
 
493
                                 b'\\+new contents\n')
 
494
 
 
495
    def test_modified_file_in_renamed_dir(self):
 
496
        """Test when a file is modified in a renamed directory."""
 
497
        tree = self.make_branch_and_tree('tree')
 
498
        self.build_tree(['tree/dir/'])
 
499
        self.build_tree_contents([('tree/dir/file', b'contents\n')])
 
500
        tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
 
501
        tree.commit('one', rev_id=b'rev-1')
 
502
 
 
503
        tree.rename_one('dir', 'other')
 
504
        self.build_tree_contents([('tree/other/file', b'new contents\n')])
 
505
        d = get_diff_as_string(tree.basis_tree(), tree)
 
506
        self.assertContainsRe(d, b"=== renamed directory 'dir' => 'other'\n")
 
507
        self.assertContainsRe(d, b"=== modified file 'other/file'\n")
 
508
        # XXX: This is technically incorrect, because it used to be at another
 
509
        # location. What to do?
 
510
        self.assertContainsRe(d, b'--- old/dir/file\t')
 
511
        self.assertContainsRe(d, b'\\+\\+\\+ new/other/file\t')
 
512
        self.assertContainsRe(d, b'-contents\n'
 
513
                                 b'\\+new contents\n')
 
514
 
 
515
    def test_renamed_directory(self):
 
516
        """Test when only a directory is only renamed."""
 
517
        tree = self.make_branch_and_tree('tree')
 
518
        self.build_tree(['tree/dir/'])
 
519
        self.build_tree_contents([('tree/dir/file', b'contents\n')])
 
520
        tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
 
521
        tree.commit('one', rev_id=b'rev-1')
 
522
 
 
523
        tree.rename_one('dir', 'newdir')
 
524
        d = get_diff_as_string(tree.basis_tree(), tree)
 
525
        # Renaming a directory should be a single "you renamed this dir" even
 
526
        # when there are files inside.
 
527
        self.assertEqual(d, b"=== renamed directory 'dir' => 'newdir'\n")
 
528
 
 
529
    def test_renamed_file(self):
 
530
        """Test when a file is only renamed."""
 
531
        tree = self.make_branch_and_tree('tree')
 
532
        self.build_tree_contents([('tree/file', b'contents\n')])
 
533
        tree.add(['file'], [b'file-id'])
 
534
        tree.commit('one', rev_id=b'rev-1')
 
535
 
 
536
        tree.rename_one('file', 'newname')
 
537
        d = get_diff_as_string(tree.basis_tree(), tree)
 
538
        self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
 
539
        # We shouldn't have a --- or +++ line, because there is no content
 
540
        # change
 
541
        self.assertNotContainsRe(d, b'---')
 
542
 
 
543
    def test_renamed_and_modified_file(self):
 
544
        """Test when a file is only renamed."""
 
545
        tree = self.make_branch_and_tree('tree')
 
546
        self.build_tree_contents([('tree/file', b'contents\n')])
 
547
        tree.add(['file'], [b'file-id'])
 
548
        tree.commit('one', rev_id=b'rev-1')
 
549
 
 
550
        tree.rename_one('file', 'newname')
 
551
        self.build_tree_contents([('tree/newname', b'new contents\n')])
 
552
        d = get_diff_as_string(tree.basis_tree(), tree)
 
553
        self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
 
554
        self.assertContainsRe(d, b'--- old/file\t')
 
555
        self.assertContainsRe(d, b'\\+\\+\\+ new/newname\t')
 
556
        self.assertContainsRe(d, b'-contents\n'
 
557
                                 b'\\+new contents\n')
 
558
 
 
559
    def test_internal_diff_exec_property(self):
 
560
        tree = self.make_branch_and_tree('tree')
 
561
 
 
562
        tt = tree.get_transform()
 
563
        tt.new_file('a', tt.root, [b'contents\n'], b'a-id', True)
 
564
        tt.new_file('b', tt.root, [b'contents\n'], b'b-id', False)
 
565
        tt.new_file('c', tt.root, [b'contents\n'], b'c-id', True)
 
566
        tt.new_file('d', tt.root, [b'contents\n'], b'd-id', False)
 
567
        tt.new_file('e', tt.root, [b'contents\n'], b'control-e-id', True)
 
568
        tt.new_file('f', tt.root, [b'contents\n'], b'control-f-id', False)
 
569
        tt.apply()
 
570
        tree.commit('one', rev_id=b'rev-1')
 
571
 
 
572
        tt = tree.get_transform()
 
573
        tt.set_executability(False, tt.trans_id_file_id(b'a-id'))
 
574
        tt.set_executability(True, tt.trans_id_file_id(b'b-id'))
 
575
        tt.set_executability(False, tt.trans_id_file_id(b'c-id'))
 
576
        tt.set_executability(True, tt.trans_id_file_id(b'd-id'))
 
577
        tt.apply()
 
578
        tree.rename_one('c', 'new-c')
 
579
        tree.rename_one('d', 'new-d')
 
580
 
 
581
        d = get_diff_as_string(tree.basis_tree(), tree)
 
582
 
 
583
        self.assertContainsRe(d, br"file 'a'.*\(properties changed:"
 
584
                                 br".*\+x to -x.*\)")
 
585
        self.assertContainsRe(d, br"file 'b'.*\(properties changed:"
 
586
                                 br".*-x to \+x.*\)")
 
587
        self.assertContainsRe(d, br"file 'c'.*\(properties changed:"
 
588
                                 br".*\+x to -x.*\)")
 
589
        self.assertContainsRe(d, br"file 'd'.*\(properties changed:"
 
590
                                 br".*-x to \+x.*\)")
 
591
        self.assertNotContainsRe(d, br"file 'e'")
 
592
        self.assertNotContainsRe(d, br"file 'f'")
 
593
 
 
594
    def test_binary_unicode_filenames(self):
 
595
        """Test that contents of files are *not* encoded in UTF-8 when there
 
596
        is a binary file in the diff.
 
597
        """
 
598
        # See https://bugs.launchpad.net/bugs/110092.
 
599
        self.requireFeature(features.UnicodeFilenameFeature)
 
600
 
 
601
        tree = self.make_branch_and_tree('tree')
 
602
        alpha, omega = u'\u03b1', u'\u03c9'
 
603
        alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
 
604
        self.build_tree_contents(
 
605
            [('tree/' + alpha, b'\0'),
 
606
             ('tree/' + omega,
 
607
              (b'The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
 
608
        tree.add([alpha], [b'file-id'])
 
609
        tree.add([omega], [b'file-id-2'])
 
610
        diff_content = StubO()
 
611
        diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
 
612
        diff_content.check_types(self, bytes)
 
613
        d = b''.join(diff_content.write_record)
 
614
        self.assertContainsRe(d, br"=== added file '%s'" % alpha_utf8)
 
615
        self.assertContainsRe(d, b"Binary files a/%s.*and b/%s.* differ\n"
 
616
                              % (alpha_utf8, alpha_utf8))
 
617
        self.assertContainsRe(d, br"=== added file '%s'" % omega_utf8)
 
618
        self.assertContainsRe(d, br"--- a/%s" % (omega_utf8,))
 
619
        self.assertContainsRe(d, br"\+\+\+ b/%s" % (omega_utf8,))
 
620
 
 
621
    def test_unicode_filename(self):
 
622
        """Test when the filename are unicode."""
 
623
        self.requireFeature(features.UnicodeFilenameFeature)
 
624
 
 
625
        alpha, omega = u'\u03b1', u'\u03c9'
 
626
        autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
 
627
 
 
628
        tree = self.make_branch_and_tree('tree')
 
629
        self.build_tree_contents([('tree/ren_' + alpha, b'contents\n')])
 
630
        tree.add(['ren_' + alpha], [b'file-id-2'])
 
631
        self.build_tree_contents([('tree/del_' + alpha, b'contents\n')])
 
632
        tree.add(['del_' + alpha], [b'file-id-3'])
 
633
        self.build_tree_contents([('tree/mod_' + alpha, b'contents\n')])
 
634
        tree.add(['mod_' + alpha], [b'file-id-4'])
 
635
 
 
636
        tree.commit('one', rev_id=b'rev-1')
 
637
 
 
638
        tree.rename_one('ren_' + alpha, 'ren_' + omega)
 
639
        tree.remove('del_' + alpha)
 
640
        self.build_tree_contents([('tree/add_' + alpha, b'contents\n')])
 
641
        tree.add(['add_' + alpha], [b'file-id'])
 
642
        self.build_tree_contents([('tree/mod_' + alpha, b'contents_mod\n')])
 
643
 
 
644
        d = get_diff_as_string(tree.basis_tree(), tree)
 
645
        self.assertContainsRe(d,
 
646
                              b"=== renamed file 'ren_%s' => 'ren_%s'\n" % (autf8, outf8))
 
647
        self.assertContainsRe(d, b"=== added file 'add_%s'" % autf8)
 
648
        self.assertContainsRe(d, b"=== modified file 'mod_%s'" % autf8)
 
649
        self.assertContainsRe(d, b"=== removed file 'del_%s'" % autf8)
 
650
 
 
651
    def test_unicode_filename_path_encoding(self):
 
652
        """Test for bug #382699: unicode filenames on Windows should be shown
 
653
        in user encoding.
 
654
        """
 
655
        self.requireFeature(features.UnicodeFilenameFeature)
 
656
        # The word 'test' in Russian
 
657
        _russian_test = u'\u0422\u0435\u0441\u0442'
 
658
        directory = _russian_test + u'/'
 
659
        test_txt = _russian_test + u'.txt'
 
660
        u1234 = u'\u1234.txt'
 
661
 
 
662
        tree = self.make_branch_and_tree('.')
 
663
        self.build_tree_contents([
 
664
            (test_txt, b'foo\n'),
 
665
            (u1234, b'foo\n'),
 
666
            (directory, None),
 
667
            ])
 
668
        tree.add([test_txt, u1234, directory])
 
669
 
 
670
        sio = BytesIO()
 
671
        diff.show_diff_trees(tree.basis_tree(), tree, sio,
 
672
                             path_encoding='cp1251')
 
673
 
 
674
        output = subst_dates(sio.getvalue())
 
675
        shouldbe = (b'''\
 
676
=== added directory '%(directory)s'
 
677
=== added file '%(test_txt)s'
 
678
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
 
679
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
 
680
@@ -0,0 +1,1 @@
 
681
+foo
 
682
 
 
683
=== added file '?.txt'
 
684
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
 
685
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
 
686
@@ -0,0 +1,1 @@
 
687
+foo
 
688
 
 
689
''' % {b'directory': _russian_test.encode('cp1251'),
 
690
            b'test_txt': test_txt.encode('cp1251'),
 
691
       })
 
692
        self.assertEqualDiff(output, shouldbe)
 
693
 
 
694
 
 
695
class DiffWasIs(diff.DiffPath):
 
696
 
 
697
    def diff(self, old_path, new_path, old_kind, new_kind):
 
698
        self.to_file.write(b'was: ')
 
699
        self.to_file.write(self.old_tree.get_file(old_path).read())
 
700
        self.to_file.write(b'is: ')
 
701
        self.to_file.write(self.new_tree.get_file(new_path).read())
 
702
 
 
703
 
 
704
class TestDiffTree(tests.TestCaseWithTransport):
 
705
 
 
706
    def setUp(self):
 
707
        super(TestDiffTree, self).setUp()
 
708
        self.old_tree = self.make_branch_and_tree('old-tree')
 
709
        self.old_tree.lock_write()
 
710
        self.addCleanup(self.old_tree.unlock)
 
711
        self.new_tree = self.make_branch_and_tree('new-tree')
 
712
        self.new_tree.lock_write()
 
713
        self.addCleanup(self.new_tree.unlock)
 
714
        self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
 
715
 
 
716
    def test_diff_text(self):
 
717
        self.build_tree_contents([('old-tree/olddir/',),
 
718
                                  ('old-tree/olddir/oldfile', b'old\n')])
 
719
        self.old_tree.add('olddir')
 
720
        self.old_tree.add('olddir/oldfile', b'file-id')
 
721
        self.build_tree_contents([('new-tree/newdir/',),
 
722
                                  ('new-tree/newdir/newfile', b'new\n')])
 
723
        self.new_tree.add('newdir')
 
724
        self.new_tree.add('newdir/newfile', b'file-id')
 
725
        differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
 
726
        differ.diff_text('olddir/oldfile', None, 'old label', 'new label')
 
727
        self.assertEqual(
 
728
            b'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
 
729
            differ.to_file.getvalue())
 
730
        differ.to_file.seek(0)
 
731
        differ.diff_text(None, 'newdir/newfile',
 
732
                         'old label', 'new label')
 
733
        self.assertEqual(
 
734
            b'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
 
735
            differ.to_file.getvalue())
 
736
        differ.to_file.seek(0)
 
737
        differ.diff_text('olddir/oldfile', 'newdir/newfile',
 
738
                         'old label', 'new label')
 
739
        self.assertEqual(
 
740
            b'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
 
741
            differ.to_file.getvalue())
 
742
 
 
743
    def test_diff_deletion(self):
 
744
        self.build_tree_contents([('old-tree/file', b'contents'),
 
745
                                  ('new-tree/file', b'contents')])
 
746
        self.old_tree.add('file', b'file-id')
 
747
        self.new_tree.add('file', b'file-id')
 
748
        os.unlink('new-tree/file')
 
749
        self.differ.show_diff(None)
 
750
        self.assertContainsRe(self.differ.to_file.getvalue(), b'-contents')
 
751
 
 
752
    def test_diff_creation(self):
 
753
        self.build_tree_contents([('old-tree/file', b'contents'),
 
754
                                  ('new-tree/file', b'contents')])
 
755
        self.old_tree.add('file', b'file-id')
 
756
        self.new_tree.add('file', b'file-id')
 
757
        os.unlink('old-tree/file')
 
758
        self.differ.show_diff(None)
 
759
        self.assertContainsRe(self.differ.to_file.getvalue(), br'\+contents')
 
760
 
 
761
    def test_diff_symlink(self):
 
762
        differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
 
763
        differ.diff_symlink('old target', None)
 
764
        self.assertEqual(b"=== target was 'old target'\n",
 
765
                         differ.to_file.getvalue())
 
766
 
 
767
        differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
 
768
        differ.diff_symlink(None, 'new target')
 
769
        self.assertEqual(b"=== target is 'new target'\n",
 
770
                         differ.to_file.getvalue())
 
771
 
 
772
        differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
 
773
        differ.diff_symlink('old target', 'new target')
 
774
        self.assertEqual(b"=== target changed 'old target' => 'new target'\n",
 
775
                         differ.to_file.getvalue())
 
776
 
 
777
    def test_diff(self):
 
778
        self.build_tree_contents([('old-tree/olddir/',),
 
779
                                  ('old-tree/olddir/oldfile', b'old\n')])
 
780
        self.old_tree.add('olddir')
 
781
        self.old_tree.add('olddir/oldfile', b'file-id')
 
782
        self.build_tree_contents([('new-tree/newdir/',),
 
783
                                  ('new-tree/newdir/newfile', b'new\n')])
 
784
        self.new_tree.add('newdir')
 
785
        self.new_tree.add('newdir/newfile', b'file-id')
 
786
        self.differ.diff('olddir/oldfile', 'newdir/newfile')
 
787
        self.assertContainsRe(
 
788
            self.differ.to_file.getvalue(),
 
789
            br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
 
790
            br' \@\@\n-old\n\+new\n\n')
 
791
 
 
792
    def test_diff_kind_change(self):
 
793
        self.requireFeature(features.SymlinkFeature)
 
794
        self.build_tree_contents([('old-tree/olddir/',),
 
795
                                  ('old-tree/olddir/oldfile', b'old\n')])
 
796
        self.old_tree.add('olddir')
 
797
        self.old_tree.add('olddir/oldfile', b'file-id')
 
798
        self.build_tree(['new-tree/newdir/'])
 
799
        os.symlink('new', 'new-tree/newdir/newfile')
 
800
        self.new_tree.add('newdir')
 
801
        self.new_tree.add('newdir/newfile', b'file-id')
 
802
        self.differ.diff('olddir/oldfile', 'newdir/newfile')
 
803
        self.assertContainsRe(
 
804
            self.differ.to_file.getvalue(),
 
805
            br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
 
806
            br' \@\@\n-old\n\n')
 
807
        self.assertContainsRe(self.differ.to_file.getvalue(),
 
808
                              b"=== target is 'new'\n")
 
809
 
 
810
    def test_diff_directory(self):
 
811
        self.build_tree(['new-tree/new-dir/'])
 
812
        self.new_tree.add('new-dir', b'new-dir-id')
 
813
        self.differ.diff(None, 'new-dir')
 
814
        self.assertEqual(self.differ.to_file.getvalue(), b'')
 
815
 
 
816
    def create_old_new(self):
 
817
        self.build_tree_contents([('old-tree/olddir/',),
 
818
                                  ('old-tree/olddir/oldfile', b'old\n')])
 
819
        self.old_tree.add('olddir')
 
820
        self.old_tree.add('olddir/oldfile', b'file-id')
 
821
        self.build_tree_contents([('new-tree/newdir/',),
 
822
                                  ('new-tree/newdir/newfile', b'new\n')])
 
823
        self.new_tree.add('newdir')
 
824
        self.new_tree.add('newdir/newfile', b'file-id')
 
825
 
 
826
    def test_register_diff(self):
 
827
        self.create_old_new()
 
828
        old_diff_factories = diff.DiffTree.diff_factories
 
829
        diff.DiffTree.diff_factories = old_diff_factories[:]
 
830
        diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
 
831
        try:
 
832
            differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
 
833
        finally:
 
834
            diff.DiffTree.diff_factories = old_diff_factories
 
835
        differ.diff('olddir/oldfile', 'newdir/newfile')
 
836
        self.assertNotContainsRe(
 
837
            differ.to_file.getvalue(),
 
838
            br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
 
839
            br' \@\@\n-old\n\+new\n\n')
 
840
        self.assertContainsRe(differ.to_file.getvalue(),
 
841
                              b'was: old\nis: new\n')
 
842
 
 
843
    def test_extra_factories(self):
 
844
        self.create_old_new()
 
845
        differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
 
846
                               extra_factories=[DiffWasIs.from_diff_tree])
 
847
        differ.diff('olddir/oldfile', 'newdir/newfile')
 
848
        self.assertNotContainsRe(
 
849
            differ.to_file.getvalue(),
 
850
            br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
 
851
            br' \@\@\n-old\n\+new\n\n')
 
852
        self.assertContainsRe(differ.to_file.getvalue(),
 
853
                              b'was: old\nis: new\n')
 
854
 
 
855
    def test_alphabetical_order(self):
 
856
        self.build_tree(['new-tree/a-file'])
 
857
        self.new_tree.add('a-file')
 
858
        self.build_tree(['old-tree/b-file'])
 
859
        self.old_tree.add('b-file')
 
860
        self.differ.show_diff(None)
 
861
        self.assertContainsRe(self.differ.to_file.getvalue(),
 
862
                              b'.*a-file(.|\n)*b-file')
 
863
 
 
864
 
 
865
class TestDiffFromTool(tests.TestCaseWithTransport):
 
866
 
 
867
    def test_from_string(self):
 
868
        diff_obj = diff.DiffFromTool.from_string(
 
869
            ['diff', '{old_path}', '{new_path}'],
 
870
            None, None, None)
 
871
        self.addCleanup(diff_obj.finish)
 
872
        self.assertEqual(['diff', '{old_path}', '{new_path}'],
 
873
                         diff_obj.command_template)
 
874
 
 
875
    def test_from_string_no_paths(self):
 
876
        diff_obj = diff.DiffFromTool.from_string(
 
877
            ['diff', "-u5"], None, None, None)
 
878
        self.addCleanup(diff_obj.finish)
 
879
        self.assertEqual(['diff', '-u5'],
 
880
                         diff_obj.command_template)
 
881
        self.assertEqual(['diff', '-u5', 'old-path', 'new-path'],
 
882
                         diff_obj._get_command('old-path', 'new-path'))
 
883
 
 
884
    def test_from_string_u5(self):
 
885
        diff_obj = diff.DiffFromTool.from_string(
 
886
            ['diff', "-u 5", '{old_path}', '{new_path}'], None, None, None)
 
887
        self.addCleanup(diff_obj.finish)
 
888
        self.assertEqual(['diff', '-u 5', '{old_path}', '{new_path}'],
 
889
                         diff_obj.command_template)
 
890
        self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
 
891
                         diff_obj._get_command('old-path', 'new-path'))
 
892
 
 
893
    def test_from_string_path_with_backslashes(self):
 
894
        self.requireFeature(features.backslashdir_feature)
 
895
        tool = ['C:\\Tools\\Diff.exe', '{old_path}', '{new_path}']
 
896
        diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
 
897
        self.addCleanup(diff_obj.finish)
 
898
        self.assertEqual(['C:\\Tools\\Diff.exe', '{old_path}', '{new_path}'],
 
899
                         diff_obj.command_template)
 
900
        self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
 
901
                         diff_obj._get_command('old-path', 'new-path'))
 
902
 
 
903
    def test_execute(self):
 
904
        output = BytesIO()
 
905
        diff_obj = diff.DiffFromTool([sys.executable, '-c',
 
906
                                      'print("{old_path} {new_path}")'],
 
907
                                     None, None, output)
 
908
        self.addCleanup(diff_obj.finish)
 
909
        diff_obj._execute('old', 'new')
 
910
        self.assertEqual(output.getvalue().rstrip(), b'old new')
 
911
 
 
912
    def test_execute_missing(self):
 
913
        diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
 
914
                                     None, None, None)
 
915
        self.addCleanup(diff_obj.finish)
 
916
        e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
 
917
                              'old', 'new')
 
918
        self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
 
919
                         ' on this machine', str(e))
 
920
 
 
921
    def test_prepare_files_creates_paths_readable_by_windows_tool(self):
 
922
        self.requireFeature(features.AttribFeature)
 
923
        output = BytesIO()
 
924
        tree = self.make_branch_and_tree('tree')
 
925
        self.build_tree_contents([('tree/file', b'content')])
 
926
        tree.add('file', b'file-id')
 
927
        tree.commit('old tree')
 
928
        tree.lock_read()
 
929
        self.addCleanup(tree.unlock)
 
930
        basis_tree = tree.basis_tree()
 
931
        basis_tree.lock_read()
 
932
        self.addCleanup(basis_tree.unlock)
 
933
        diff_obj = diff.DiffFromTool([sys.executable, '-c',
 
934
                                      'print "{old_path} {new_path}"'],
 
935
                                     basis_tree, tree, output)
 
936
        diff_obj._prepare_files('file', 'file', file_id=b'file-id')
 
937
        # The old content should be readonly
 
938
        self.assertReadableByAttrib(diff_obj._root, 'old\\file',
 
939
                                    r'R.*old\\file$')
 
940
        # The new content should use the tree object, not a 'new' file anymore
 
941
        self.assertEndsWith(tree.basedir, 'work/tree')
 
942
        self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
 
943
 
 
944
    def assertReadableByAttrib(self, cwd, relpath, regex):
 
945
        proc = subprocess.Popen(['attrib', relpath],
 
946
                                stdout=subprocess.PIPE,
 
947
                                cwd=cwd)
 
948
        (result, err) = proc.communicate()
 
949
        self.assertContainsRe(result.replace('\r\n', '\n'), regex)
 
950
 
 
951
    def test_prepare_files(self):
 
952
        output = BytesIO()
 
953
        tree = self.make_branch_and_tree('tree')
 
954
        self.build_tree_contents([('tree/oldname', b'oldcontent')])
 
955
        self.build_tree_contents([('tree/oldname2', b'oldcontent2')])
 
956
        tree.add('oldname', b'file-id')
 
957
        tree.add('oldname2', b'file2-id')
 
958
        # Earliest allowable date on FAT32 filesystems is 1980-01-01
 
959
        tree.commit('old tree', timestamp=315532800)
 
960
        tree.rename_one('oldname', 'newname')
 
961
        tree.rename_one('oldname2', 'newname2')
 
962
        self.build_tree_contents([('tree/newname', b'newcontent')])
 
963
        self.build_tree_contents([('tree/newname2', b'newcontent2')])
 
964
        old_tree = tree.basis_tree()
 
965
        old_tree.lock_read()
 
966
        self.addCleanup(old_tree.unlock)
 
967
        tree.lock_read()
 
968
        self.addCleanup(tree.unlock)
 
969
        diff_obj = diff.DiffFromTool([sys.executable, '-c',
 
970
                                      'print "{old_path} {new_path}"'],
 
971
                                     old_tree, tree, output)
 
972
        self.addCleanup(diff_obj.finish)
 
973
        self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
 
974
        old_path, new_path = diff_obj._prepare_files(
 
975
            'oldname', 'newname')
 
976
        self.assertContainsRe(old_path, 'old/oldname$')
 
977
        self.assertEqual(315532800, os.stat(old_path).st_mtime)
 
978
        self.assertContainsRe(new_path, 'tree/newname$')
 
979
        self.assertFileEqual(b'oldcontent', old_path)
 
980
        self.assertFileEqual(b'newcontent', new_path)
 
981
        if osutils.host_os_dereferences_symlinks():
 
982
            self.assertTrue(os.path.samefile('tree/newname', new_path))
 
983
        # make sure we can create files with the same parent directories
 
984
        diff_obj._prepare_files('oldname2', 'newname2')
 
985
 
 
986
 
 
987
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
 
988
 
 
989
    def test_encodable_filename(self):
 
990
        # Just checks file path for external diff tool.
 
991
        # We cannot change CPython's internal encoding used by os.exec*.
 
992
        diffobj = diff.DiffFromTool(['dummy', '{old_path}', '{new_path}'],
 
993
                                    None, None, None)
 
994
        for _, scenario in EncodingAdapter.encoding_scenarios:
 
995
            encoding = scenario['encoding']
 
996
            dirname = scenario['info']['directory']
 
997
            filename = scenario['info']['filename']
 
998
 
 
999
            self.overrideAttr(diffobj, '_fenc', lambda: encoding)
 
1000
            relpath = dirname + u'/' + filename
 
1001
            fullpath = diffobj._safe_filename('safe', relpath)
 
1002
            self.assertEqual(fullpath,
 
1003
                             fullpath.encode(encoding).decode(encoding))
 
1004
            self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
 
1005
 
 
1006
    def test_unencodable_filename(self):
 
1007
        diffobj = diff.DiffFromTool(['dummy', '{old_path}', '{new_path}'],
 
1008
                                    None, None, None)
 
1009
        for _, scenario in EncodingAdapter.encoding_scenarios:
 
1010
            encoding = scenario['encoding']
 
1011
            dirname = scenario['info']['directory']
 
1012
            filename = scenario['info']['filename']
 
1013
 
 
1014
            if encoding == 'iso-8859-1':
 
1015
                encoding = 'iso-8859-2'
 
1016
            else:
 
1017
                encoding = 'iso-8859-1'
 
1018
 
 
1019
            self.overrideAttr(diffobj, '_fenc', lambda: encoding)
 
1020
            relpath = dirname + u'/' + filename
 
1021
            fullpath = diffobj._safe_filename('safe', relpath)
 
1022
            self.assertEqual(fullpath,
 
1023
                             fullpath.encode(encoding).decode(encoding))
 
1024
            self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
 
1025
 
 
1026
 
 
1027
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
 
1028
 
 
1029
    def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
 
1030
        """Call get_trees_and_branches_to_diff_locked."""
 
1031
        exit_stack = contextlib.ExitStack()
 
1032
        self.addCleanup(exit_stack.close)
 
1033
        return diff.get_trees_and_branches_to_diff_locked(
 
1034
            path_list, revision_specs, old_url, new_url, exit_stack)
 
1035
 
 
1036
    def test_basic(self):
 
1037
        tree = self.make_branch_and_tree('tree')
 
1038
        (old_tree, new_tree,
 
1039
         old_branch, new_branch,
 
1040
         specific_files, extra_trees) = self.call_gtabtd(
 
1041
             ['tree'], None, None, None)
 
1042
 
 
1043
        self.assertIsInstance(old_tree, revisiontree.RevisionTree)
 
1044
        self.assertEqual(_mod_revision.NULL_REVISION,
 
1045
                         old_tree.get_revision_id())
 
1046
        self.assertEqual(tree.basedir, new_tree.basedir)
 
1047
        self.assertEqual(tree.branch.base, old_branch.base)
 
1048
        self.assertEqual(tree.branch.base, new_branch.base)
 
1049
        self.assertIs(None, specific_files)
 
1050
        self.assertIs(None, extra_trees)
 
1051
 
 
1052
    def test_with_rev_specs(self):
 
1053
        tree = self.make_branch_and_tree('tree')
 
1054
        self.build_tree_contents([('tree/file', b'oldcontent')])
 
1055
        tree.add('file', b'file-id')
 
1056
        tree.commit('old tree', timestamp=0, rev_id=b"old-id")
 
1057
        self.build_tree_contents([('tree/file', b'newcontent')])
 
1058
        tree.commit('new tree', timestamp=0, rev_id=b"new-id")
 
1059
 
 
1060
        revisions = [revisionspec.RevisionSpec.from_string('1'),
 
1061
                     revisionspec.RevisionSpec.from_string('2')]
 
1062
        (old_tree, new_tree,
 
1063
         old_branch, new_branch,
 
1064
         specific_files, extra_trees) = self.call_gtabtd(
 
1065
            ['tree'], revisions, None, None)
 
1066
 
 
1067
        self.assertIsInstance(old_tree, revisiontree.RevisionTree)
 
1068
        self.assertEqual(b"old-id", old_tree.get_revision_id())
 
1069
        self.assertIsInstance(new_tree, revisiontree.RevisionTree)
 
1070
        self.assertEqual(b"new-id", new_tree.get_revision_id())
 
1071
        self.assertEqual(tree.branch.base, old_branch.base)
 
1072
        self.assertEqual(tree.branch.base, new_branch.base)
 
1073
        self.assertIs(None, specific_files)
 
1074
        self.assertEqual(tree.basedir, extra_trees[0].basedir)