1
from bzrlib.tests import TestCase
2
from bzrlib.diff import internal_diff
3
from cStringIO import StringIO
4
def udiff_lines(old, new):
6
internal_diff('old', old, 'new', new, output)
1
# Copyright (C) 2005-2012, 2014, 2016, 2017 Canonical Ltd
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.
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.
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
27
revision as _mod_revision,
32
from ..sixish import (
40
from ..tests.scenarios import load_tests_apply_scenarios
43
load_tests = load_tests_apply_scenarios
46
def subst_dates(string):
47
"""Replace date strings with constant values."""
48
return re.sub(br'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [-\+]\d{4}',
49
b'YYYY-MM-DD HH:MM:SS +ZZZZ', string)
52
def udiff_lines(old, new, allow_binary=False):
54
diff.internal_diff('old', old, 'new', new, output, allow_binary)
8
56
return output.readlines()
10
class TestDiff(TestCase):
59
def external_udiff_lines(old, new, use_stringio=False):
61
# BytesIO has no fileno, so it tests a different codepath
64
output = tempfile.TemporaryFile()
66
diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
68
raise tests.TestSkipped('external "diff" not present to test')
70
lines = output.readlines()
76
"""Simple file-like object that allows writes with any type and records."""
79
self.write_record = []
81
def write(self, data):
82
self.write_record.append(data)
84
def check_types(self, testcase, expected_type):
86
any(not isinstance(o, expected_type) for o in self.write_record),
87
"Not all writes of type %s: %r" % (
88
expected_type.__name__, self.write_record))
91
class TestDiffOptions(tests.TestCase):
93
def test_unified_added(self):
94
"""Check for default style '-u' only if no other style specified
97
# Verify that style defaults to unified, id est '-u' appended
98
# to option list, in the absence of an alternative style.
99
self.assertEqual(['-a', '-u'], diff.default_style_unified(['-a']))
102
class TestDiffOptionsScenarios(tests.TestCase):
104
scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
105
style = None # Set by load_tests_apply_scenarios from scenarios
107
def test_unified_not_added(self):
108
# Verify that for all valid style options, '-u' is not
109
# appended to option list.
110
ret_opts = diff.default_style_unified(diff_opts=["%s" % (self.style,)])
111
self.assertEqual(["%s" % (self.style,)], ret_opts)
114
class TestDiff(tests.TestCase):
11
116
def test_add_nl(self):
12
117
"""diff generates a valid diff for patches that add a newline"""
13
lines = udiff_lines(['boo'], ['boo\n'])
118
lines = udiff_lines([b'boo'], [b'boo\n'])
14
119
self.check_patch(lines)
15
self.assertEquals(lines[4], '\\ No newline at end of file\n')
16
## "expected no-nl, got %r" % lines[4]
120
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
121
## "expected no-nl, got %r" % lines[4]
18
123
def test_add_nl_2(self):
19
124
"""diff generates a valid diff for patches that change last line and
22
lines = udiff_lines(['boo'], ['goo\n'])
127
lines = udiff_lines([b'boo'], [b'goo\n'])
23
128
self.check_patch(lines)
24
self.assertEquals(lines[4], '\\ No newline at end of file\n')
25
## "expected no-nl, got %r" % lines[4]
129
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
130
## "expected no-nl, got %r" % lines[4]
27
132
def test_remove_nl(self):
28
133
"""diff generates a valid diff for patches that change last line and
31
lines = udiff_lines(['boo\n'], ['boo'])
136
lines = udiff_lines([b'boo\n'], [b'boo'])
32
137
self.check_patch(lines)
33
self.assertEquals(lines[5], '\\ No newline at end of file\n')
34
## "expected no-nl, got %r" % lines[5]
138
self.assertEqual(lines[5], b'\\ No newline at end of file\n')
139
## "expected no-nl, got %r" % lines[5]
36
141
def check_patch(self, lines):
37
self.assert_(len(lines) > 1)
38
## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
39
self.assert_(lines[0].startswith ('---'))
40
## 'No orig line for patch:\n%s' % "".join(lines)
41
self.assert_(lines[1].startswith ('+++'))
42
## 'No mod line for patch:\n%s' % "".join(lines)
43
self.assert_(len(lines) > 2)
44
## "No hunks for patch:\n%s" % "".join(lines)
45
self.assert_(lines[2].startswith('@@'))
46
## "No hunk header for patch:\n%s" % "".join(lines)
47
self.assert_('@@' in lines[2][2:])
48
## "Unterminated hunk header for patch:\n%s" % "".join(lines)
142
self.assertTrue(len(lines) > 1)
143
## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
144
self.assertTrue(lines[0].startswith(b'---'))
145
## 'No orig line for patch:\n%s' % "".join(lines)
146
self.assertTrue(lines[1].startswith(b'+++'))
147
## 'No mod line for patch:\n%s' % "".join(lines)
148
self.assertTrue(len(lines) > 2)
149
## "No hunks for patch:\n%s" % "".join(lines)
150
self.assertTrue(lines[2].startswith(b'@@'))
151
## "No hunk header for patch:\n%s" % "".join(lines)
152
self.assertTrue(b'@@' in lines[2][2:])
153
## "Unterminated hunk header for patch:\n%s" % "".join(lines)
155
def test_binary_lines(self):
157
uni_lines = [1023 * b'a' + b'\x00']
158
self.assertRaises(errors.BinaryFile, udiff_lines, uni_lines, empty)
159
self.assertRaises(errors.BinaryFile, udiff_lines, empty, uni_lines)
160
udiff_lines(uni_lines, empty, allow_binary=True)
161
udiff_lines(empty, uni_lines, allow_binary=True)
163
def test_external_diff(self):
164
lines = external_udiff_lines([b'boo\n'], [b'goo\n'])
165
self.check_patch(lines)
166
self.assertEqual(b'\n', lines[-1])
168
def test_external_diff_no_fileno(self):
169
# Make sure that we can handle not having a fileno, even
170
# if the diff is large
171
lines = external_udiff_lines([b'boo\n'] * 10000,
174
self.check_patch(lines)
176
def test_external_diff_binary_lang_c(self):
177
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
178
self.overrideEnv(lang, 'C')
179
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
180
# Older versions of diffutils say "Binary files", newer
181
# versions just say "Files".
182
self.assertContainsRe(
183
lines[0], b'(Binary f|F)iles old and new differ\n')
184
self.assertEqual(lines[1:], [b'\n'])
186
def test_no_external_diff(self):
187
"""Check that NoDiff is raised when diff is not available"""
188
# Make sure no 'diff' command is available
189
# XXX: Weird, using None instead of '' breaks the test -- vila 20101216
190
self.overrideEnv('PATH', '')
191
self.assertRaises(errors.NoDiff, diff.external_diff,
192
b'old', [b'boo\n'], b'new', [b'goo\n'],
193
BytesIO(), diff_opts=['-u'])
195
def test_internal_diff_default(self):
196
# Default internal diff encoding is utf8
198
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
199
u'new_\xe5', [b'new_text\n'], output)
200
lines = output.getvalue().splitlines(True)
201
self.check_patch(lines)
202
self.assertEqual([b'--- old_\xc2\xb5\n',
203
b'+++ new_\xc3\xa5\n',
204
b'@@ -1,1 +1,1 @@\n',
210
def test_internal_diff_utf8(self):
212
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
213
u'new_\xe5', [b'new_text\n'], output,
214
path_encoding='utf8')
215
lines = output.getvalue().splitlines(True)
216
self.check_patch(lines)
217
self.assertEqual([b'--- old_\xc2\xb5\n',
218
b'+++ new_\xc3\xa5\n',
219
b'@@ -1,1 +1,1 @@\n',
225
def test_internal_diff_iso_8859_1(self):
227
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
228
u'new_\xe5', [b'new_text\n'], output,
229
path_encoding='iso-8859-1')
230
lines = output.getvalue().splitlines(True)
231
self.check_patch(lines)
232
self.assertEqual([b'--- old_\xb5\n',
234
b'@@ -1,1 +1,1 @@\n',
240
def test_internal_diff_no_content(self):
242
diff.internal_diff(u'old', [], u'new', [], output)
243
self.assertEqual(b'', output.getvalue())
245
def test_internal_diff_no_changes(self):
247
diff.internal_diff(u'old', [b'text\n', b'contents\n'],
248
u'new', [b'text\n', b'contents\n'],
250
self.assertEqual(b'', output.getvalue())
252
def test_internal_diff_returns_bytes(self):
254
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
255
u'new_\xe5', [b'new_text\n'], output)
256
output.check_types(self, bytes)
258
def test_internal_diff_default_context(self):
260
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
261
b'same_text\n', b'same_text\n', b'old_text\n'],
262
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
263
b'same_text\n', b'same_text\n', b'new_text\n'], output)
264
lines = output.getvalue().splitlines(True)
265
self.check_patch(lines)
266
self.assertEqual([b'--- old\n',
268
b'@@ -3,4 +3,4 @@\n',
277
def test_internal_diff_no_context(self):
279
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
280
b'same_text\n', b'same_text\n', b'old_text\n'],
281
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
282
b'same_text\n', b'same_text\n', b'new_text\n'], output,
284
lines = output.getvalue().splitlines(True)
285
self.check_patch(lines)
286
self.assertEqual([b'--- old\n',
288
b'@@ -6,1 +6,1 @@\n',
294
def test_internal_diff_more_context(self):
296
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
297
b'same_text\n', b'same_text\n', b'old_text\n'],
298
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
299
b'same_text\n', b'same_text\n', b'new_text\n'], output,
301
lines = output.getvalue().splitlines(True)
302
self.check_patch(lines)
303
self.assertEqual([b'--- old\n',
305
b'@@ -2,5 +2,5 @@\n',
316
class TestDiffFiles(tests.TestCaseInTempDir):
318
def test_external_diff_binary(self):
319
"""The output when using external diff should use diff's i18n error"""
320
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
321
self.overrideEnv(lang, 'C')
322
# Make sure external_diff doesn't fail in the current LANG
323
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
325
cmd = ['diff', '-u', '--binary', 'old', 'new']
326
with open('old', 'wb') as f:
327
f.write(b'\x00foobar\n')
328
with open('new', 'wb') as f:
329
f.write(b'foo\x00bar\n')
330
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
331
stdin=subprocess.PIPE)
332
out, err = pipe.communicate()
333
# We should output whatever diff tells us, plus a trailing newline
334
self.assertEqual(out.splitlines(True) + [b'\n'], lines)
337
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
339
if working_tree is not None:
340
extra_trees = (working_tree,)
343
diff.show_diff_trees(tree1, tree2, output,
344
specific_files=specific_files,
345
extra_trees=extra_trees, old_label='old/',
347
return output.getvalue()
350
class TestDiffDates(tests.TestCaseWithTransport):
353
super(TestDiffDates, self).setUp()
354
self.wt = self.make_branch_and_tree('.')
355
self.b = self.wt.branch
356
self.build_tree_contents([
357
('file1', b'file1 contents at rev 1\n'),
358
('file2', b'file2 contents at rev 1\n')
360
self.wt.add(['file1', 'file2'])
362
message='Revision 1',
363
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
366
self.build_tree_contents([('file1', b'file1 contents at rev 2\n')])
368
message='Revision 2',
369
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
372
self.build_tree_contents([('file2', b'file2 contents at rev 3\n')])
374
message='Revision 3',
375
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
378
self.wt.remove(['file2'])
380
message='Revision 4',
381
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
384
self.build_tree_contents([
385
('file1', b'file1 contents in working tree\n')
387
# set the date stamps for files in the working tree to known values
388
os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
390
def test_diff_rev_tree_working_tree(self):
391
output = get_diff_as_string(self.wt.basis_tree(), self.wt)
392
# note that the date for old/file1 is from rev 2 rather than from
393
# the basis revision (rev 4)
394
self.assertEqualDiff(output, b'''\
395
=== modified file 'file1'
396
--- old/file1\t2006-04-02 00:00:00 +0000
397
+++ new/file1\t2006-04-05 00:00:00 +0000
399
-file1 contents at rev 2
400
+file1 contents in working tree
404
def test_diff_rev_tree_rev_tree(self):
405
tree1 = self.b.repository.revision_tree(b'rev-2')
406
tree2 = self.b.repository.revision_tree(b'rev-3')
407
output = get_diff_as_string(tree1, tree2)
408
self.assertEqualDiff(output, b'''\
409
=== modified file 'file2'
410
--- old/file2\t2006-04-01 00:00:00 +0000
411
+++ new/file2\t2006-04-03 00:00:00 +0000
413
-file2 contents at rev 1
414
+file2 contents at rev 3
418
def test_diff_add_files(self):
419
tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
420
tree2 = self.b.repository.revision_tree(b'rev-1')
421
output = get_diff_as_string(tree1, tree2)
422
# the files have the epoch time stamp for the tree in which
424
self.assertEqualDiff(output, b'''\
425
=== added file 'file1'
426
--- old/file1\t1970-01-01 00:00:00 +0000
427
+++ new/file1\t2006-04-01 00:00:00 +0000
429
+file1 contents at rev 1
431
=== added file 'file2'
432
--- old/file2\t1970-01-01 00:00:00 +0000
433
+++ new/file2\t2006-04-01 00:00:00 +0000
435
+file2 contents at rev 1
439
def test_diff_remove_files(self):
440
tree1 = self.b.repository.revision_tree(b'rev-3')
441
tree2 = self.b.repository.revision_tree(b'rev-4')
442
output = get_diff_as_string(tree1, tree2)
443
# the file has the epoch time stamp for the tree in which
445
self.assertEqualDiff(output, b'''\
446
=== removed file 'file2'
447
--- old/file2\t2006-04-03 00:00:00 +0000
448
+++ new/file2\t1970-01-01 00:00:00 +0000
450
-file2 contents at rev 3
454
def test_show_diff_specified(self):
455
"""A working tree filename can be used to identify a file"""
456
self.wt.rename_one('file1', 'file1b')
457
old_tree = self.b.repository.revision_tree(b'rev-1')
458
new_tree = self.b.repository.revision_tree(b'rev-4')
459
out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
460
working_tree=self.wt)
461
self.assertContainsRe(out, b'file1\t')
463
def test_recursive_diff(self):
464
"""Children of directories are matched"""
467
self.wt.add(['dir1', 'dir2'])
468
self.wt.rename_one('file1', 'dir1/file1')
469
old_tree = self.b.repository.revision_tree(b'rev-1')
470
new_tree = self.b.repository.revision_tree(b'rev-4')
471
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
472
working_tree=self.wt)
473
self.assertContainsRe(out, b'file1\t')
474
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
475
working_tree=self.wt)
476
self.assertNotContainsRe(out, b'file1\t')
479
class TestShowDiffTrees(tests.TestCaseWithTransport):
480
"""Direct tests for show_diff_trees"""
482
def test_modified_file(self):
483
"""Test when a file is modified."""
484
tree = self.make_branch_and_tree('tree')
485
self.build_tree_contents([('tree/file', b'contents\n')])
486
tree.add(['file'], [b'file-id'])
487
tree.commit('one', rev_id=b'rev-1')
489
self.build_tree_contents([('tree/file', b'new contents\n')])
490
d = get_diff_as_string(tree.basis_tree(), tree)
491
self.assertContainsRe(d, b"=== modified file 'file'\n")
492
self.assertContainsRe(d, b'--- old/file\t')
493
self.assertContainsRe(d, b'\\+\\+\\+ new/file\t')
494
self.assertContainsRe(d, b'-contents\n'
495
b'\\+new contents\n')
497
def test_modified_file_in_renamed_dir(self):
498
"""Test when a file is modified in a renamed directory."""
499
tree = self.make_branch_and_tree('tree')
500
self.build_tree(['tree/dir/'])
501
self.build_tree_contents([('tree/dir/file', b'contents\n')])
502
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
503
tree.commit('one', rev_id=b'rev-1')
505
tree.rename_one('dir', 'other')
506
self.build_tree_contents([('tree/other/file', b'new contents\n')])
507
d = get_diff_as_string(tree.basis_tree(), tree)
508
self.assertContainsRe(d, b"=== renamed directory 'dir' => 'other'\n")
509
self.assertContainsRe(d, b"=== modified file 'other/file'\n")
510
# XXX: This is technically incorrect, because it used to be at another
511
# location. What to do?
512
self.assertContainsRe(d, b'--- old/dir/file\t')
513
self.assertContainsRe(d, b'\\+\\+\\+ new/other/file\t')
514
self.assertContainsRe(d, b'-contents\n'
515
b'\\+new contents\n')
517
def test_renamed_directory(self):
518
"""Test when only a directory is only renamed."""
519
tree = self.make_branch_and_tree('tree')
520
self.build_tree(['tree/dir/'])
521
self.build_tree_contents([('tree/dir/file', b'contents\n')])
522
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
523
tree.commit('one', rev_id=b'rev-1')
525
tree.rename_one('dir', 'newdir')
526
d = get_diff_as_string(tree.basis_tree(), tree)
527
# Renaming a directory should be a single "you renamed this dir" even
528
# when there are files inside.
529
self.assertEqual(d, b"=== renamed directory 'dir' => 'newdir'\n")
531
def test_renamed_file(self):
532
"""Test when a file is only renamed."""
533
tree = self.make_branch_and_tree('tree')
534
self.build_tree_contents([('tree/file', b'contents\n')])
535
tree.add(['file'], [b'file-id'])
536
tree.commit('one', rev_id=b'rev-1')
538
tree.rename_one('file', 'newname')
539
d = get_diff_as_string(tree.basis_tree(), tree)
540
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
541
# We shouldn't have a --- or +++ line, because there is no content
543
self.assertNotContainsRe(d, b'---')
545
def test_renamed_and_modified_file(self):
546
"""Test when a file is only renamed."""
547
tree = self.make_branch_and_tree('tree')
548
self.build_tree_contents([('tree/file', b'contents\n')])
549
tree.add(['file'], [b'file-id'])
550
tree.commit('one', rev_id=b'rev-1')
552
tree.rename_one('file', 'newname')
553
self.build_tree_contents([('tree/newname', b'new contents\n')])
554
d = get_diff_as_string(tree.basis_tree(), tree)
555
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
556
self.assertContainsRe(d, b'--- old/file\t')
557
self.assertContainsRe(d, b'\\+\\+\\+ new/newname\t')
558
self.assertContainsRe(d, b'-contents\n'
559
b'\\+new contents\n')
561
def test_internal_diff_exec_property(self):
562
tree = self.make_branch_and_tree('tree')
564
tt = tree.get_transform()
565
tt.new_file('a', tt.root, [b'contents\n'], b'a-id', True)
566
tt.new_file('b', tt.root, [b'contents\n'], b'b-id', False)
567
tt.new_file('c', tt.root, [b'contents\n'], b'c-id', True)
568
tt.new_file('d', tt.root, [b'contents\n'], b'd-id', False)
569
tt.new_file('e', tt.root, [b'contents\n'], b'control-e-id', True)
570
tt.new_file('f', tt.root, [b'contents\n'], b'control-f-id', False)
572
tree.commit('one', rev_id=b'rev-1')
574
tt = tree.get_transform()
575
tt.set_executability(False, tt.trans_id_file_id(b'a-id'))
576
tt.set_executability(True, tt.trans_id_file_id(b'b-id'))
577
tt.set_executability(False, tt.trans_id_file_id(b'c-id'))
578
tt.set_executability(True, tt.trans_id_file_id(b'd-id'))
580
tree.rename_one('c', 'new-c')
581
tree.rename_one('d', 'new-d')
583
d = get_diff_as_string(tree.basis_tree(), tree)
585
self.assertContainsRe(d, br"file 'a'.*\(properties changed:"
587
self.assertContainsRe(d, br"file 'b'.*\(properties changed:"
589
self.assertContainsRe(d, br"file 'c'.*\(properties changed:"
591
self.assertContainsRe(d, br"file 'd'.*\(properties changed:"
593
self.assertNotContainsRe(d, br"file 'e'")
594
self.assertNotContainsRe(d, br"file 'f'")
596
def test_binary_unicode_filenames(self):
597
"""Test that contents of files are *not* encoded in UTF-8 when there
598
is a binary file in the diff.
600
# See https://bugs.launchpad.net/bugs/110092.
601
self.requireFeature(features.UnicodeFilenameFeature)
603
tree = self.make_branch_and_tree('tree')
604
alpha, omega = u'\u03b1', u'\u03c9'
605
alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
606
self.build_tree_contents(
607
[('tree/' + alpha, b'\0'),
609
(b'The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
610
tree.add([alpha], [b'file-id'])
611
tree.add([omega], [b'file-id-2'])
612
diff_content = StubO()
613
diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
614
diff_content.check_types(self, bytes)
615
d = b''.join(diff_content.write_record)
616
self.assertContainsRe(d, br"=== added file '%s'" % alpha_utf8)
617
self.assertContainsRe(d, b"Binary files a/%s.*and b/%s.* differ\n"
618
% (alpha_utf8, alpha_utf8))
619
self.assertContainsRe(d, br"=== added file '%s'" % omega_utf8)
620
self.assertContainsRe(d, br"--- a/%s" % (omega_utf8,))
621
self.assertContainsRe(d, br"\+\+\+ b/%s" % (omega_utf8,))
623
def test_unicode_filename(self):
624
"""Test when the filename are unicode."""
625
self.requireFeature(features.UnicodeFilenameFeature)
627
alpha, omega = u'\u03b1', u'\u03c9'
628
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
630
tree = self.make_branch_and_tree('tree')
631
self.build_tree_contents([('tree/ren_' + alpha, b'contents\n')])
632
tree.add(['ren_' + alpha], [b'file-id-2'])
633
self.build_tree_contents([('tree/del_' + alpha, b'contents\n')])
634
tree.add(['del_' + alpha], [b'file-id-3'])
635
self.build_tree_contents([('tree/mod_' + alpha, b'contents\n')])
636
tree.add(['mod_' + alpha], [b'file-id-4'])
638
tree.commit('one', rev_id=b'rev-1')
640
tree.rename_one('ren_' + alpha, 'ren_' + omega)
641
tree.remove('del_' + alpha)
642
self.build_tree_contents([('tree/add_' + alpha, b'contents\n')])
643
tree.add(['add_' + alpha], [b'file-id'])
644
self.build_tree_contents([('tree/mod_' + alpha, b'contents_mod\n')])
646
d = get_diff_as_string(tree.basis_tree(), tree)
647
self.assertContainsRe(d,
648
b"=== renamed file 'ren_%s' => 'ren_%s'\n" % (autf8, outf8))
649
self.assertContainsRe(d, b"=== added file 'add_%s'" % autf8)
650
self.assertContainsRe(d, b"=== modified file 'mod_%s'" % autf8)
651
self.assertContainsRe(d, b"=== removed file 'del_%s'" % autf8)
653
def test_unicode_filename_path_encoding(self):
654
"""Test for bug #382699: unicode filenames on Windows should be shown
657
self.requireFeature(features.UnicodeFilenameFeature)
658
# The word 'test' in Russian
659
_russian_test = u'\u0422\u0435\u0441\u0442'
660
directory = _russian_test + u'/'
661
test_txt = _russian_test + u'.txt'
662
u1234 = u'\u1234.txt'
664
tree = self.make_branch_and_tree('.')
665
self.build_tree_contents([
666
(test_txt, b'foo\n'),
670
tree.add([test_txt, u1234, directory])
673
diff.show_diff_trees(tree.basis_tree(), tree, sio,
674
path_encoding='cp1251')
676
output = subst_dates(sio.getvalue())
678
=== added directory '%(directory)s'
679
=== added file '%(test_txt)s'
680
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
681
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
685
=== added file '?.txt'
686
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
687
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
691
''' % {b'directory': _russian_test.encode('cp1251'),
692
b'test_txt': test_txt.encode('cp1251'),
694
self.assertEqualDiff(output, shouldbe)
697
class DiffWasIs(diff.DiffPath):
699
def diff(self, old_path, new_path, old_kind, new_kind):
700
self.to_file.write(b'was: ')
701
self.to_file.write(self.old_tree.get_file(old_path).read())
702
self.to_file.write(b'is: ')
703
self.to_file.write(self.new_tree.get_file(new_path).read())
706
class TestDiffTree(tests.TestCaseWithTransport):
709
super(TestDiffTree, self).setUp()
710
self.old_tree = self.make_branch_and_tree('old-tree')
711
self.old_tree.lock_write()
712
self.addCleanup(self.old_tree.unlock)
713
self.new_tree = self.make_branch_and_tree('new-tree')
714
self.new_tree.lock_write()
715
self.addCleanup(self.new_tree.unlock)
716
self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
718
def test_diff_text(self):
719
self.build_tree_contents([('old-tree/olddir/',),
720
('old-tree/olddir/oldfile', b'old\n')])
721
self.old_tree.add('olddir')
722
self.old_tree.add('olddir/oldfile', b'file-id')
723
self.build_tree_contents([('new-tree/newdir/',),
724
('new-tree/newdir/newfile', b'new\n')])
725
self.new_tree.add('newdir')
726
self.new_tree.add('newdir/newfile', b'file-id')
727
differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
728
differ.diff_text('olddir/oldfile', None, 'old label', 'new label')
730
b'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
731
differ.to_file.getvalue())
732
differ.to_file.seek(0)
733
differ.diff_text(None, 'newdir/newfile',
734
'old label', 'new label')
736
b'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
737
differ.to_file.getvalue())
738
differ.to_file.seek(0)
739
differ.diff_text('olddir/oldfile', 'newdir/newfile',
740
'old label', 'new label')
742
b'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
743
differ.to_file.getvalue())
745
def test_diff_deletion(self):
746
self.build_tree_contents([('old-tree/file', b'contents'),
747
('new-tree/file', b'contents')])
748
self.old_tree.add('file', b'file-id')
749
self.new_tree.add('file', b'file-id')
750
os.unlink('new-tree/file')
751
self.differ.show_diff(None)
752
self.assertContainsRe(self.differ.to_file.getvalue(), b'-contents')
754
def test_diff_creation(self):
755
self.build_tree_contents([('old-tree/file', b'contents'),
756
('new-tree/file', b'contents')])
757
self.old_tree.add('file', b'file-id')
758
self.new_tree.add('file', b'file-id')
759
os.unlink('old-tree/file')
760
self.differ.show_diff(None)
761
self.assertContainsRe(self.differ.to_file.getvalue(), br'\+contents')
763
def test_diff_symlink(self):
764
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
765
differ.diff_symlink('old target', None)
766
self.assertEqual(b"=== target was 'old target'\n",
767
differ.to_file.getvalue())
769
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
770
differ.diff_symlink(None, 'new target')
771
self.assertEqual(b"=== target is 'new target'\n",
772
differ.to_file.getvalue())
774
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
775
differ.diff_symlink('old target', 'new target')
776
self.assertEqual(b"=== target changed 'old target' => 'new target'\n",
777
differ.to_file.getvalue())
780
self.build_tree_contents([('old-tree/olddir/',),
781
('old-tree/olddir/oldfile', b'old\n')])
782
self.old_tree.add('olddir')
783
self.old_tree.add('olddir/oldfile', b'file-id')
784
self.build_tree_contents([('new-tree/newdir/',),
785
('new-tree/newdir/newfile', b'new\n')])
786
self.new_tree.add('newdir')
787
self.new_tree.add('newdir/newfile', b'file-id')
788
self.differ.diff('olddir/oldfile', 'newdir/newfile')
789
self.assertContainsRe(
790
self.differ.to_file.getvalue(),
791
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
792
br' \@\@\n-old\n\+new\n\n')
794
def test_diff_kind_change(self):
795
self.requireFeature(features.SymlinkFeature)
796
self.build_tree_contents([('old-tree/olddir/',),
797
('old-tree/olddir/oldfile', b'old\n')])
798
self.old_tree.add('olddir')
799
self.old_tree.add('olddir/oldfile', b'file-id')
800
self.build_tree(['new-tree/newdir/'])
801
os.symlink('new', 'new-tree/newdir/newfile')
802
self.new_tree.add('newdir')
803
self.new_tree.add('newdir/newfile', b'file-id')
804
self.differ.diff('olddir/oldfile', 'newdir/newfile')
805
self.assertContainsRe(
806
self.differ.to_file.getvalue(),
807
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
809
self.assertContainsRe(self.differ.to_file.getvalue(),
810
b"=== target is 'new'\n")
812
def test_diff_directory(self):
813
self.build_tree(['new-tree/new-dir/'])
814
self.new_tree.add('new-dir', b'new-dir-id')
815
self.differ.diff(None, 'new-dir')
816
self.assertEqual(self.differ.to_file.getvalue(), b'')
818
def create_old_new(self):
819
self.build_tree_contents([('old-tree/olddir/',),
820
('old-tree/olddir/oldfile', b'old\n')])
821
self.old_tree.add('olddir')
822
self.old_tree.add('olddir/oldfile', b'file-id')
823
self.build_tree_contents([('new-tree/newdir/',),
824
('new-tree/newdir/newfile', b'new\n')])
825
self.new_tree.add('newdir')
826
self.new_tree.add('newdir/newfile', b'file-id')
828
def test_register_diff(self):
829
self.create_old_new()
830
old_diff_factories = diff.DiffTree.diff_factories
831
diff.DiffTree.diff_factories = old_diff_factories[:]
832
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
834
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
836
diff.DiffTree.diff_factories = old_diff_factories
837
differ.diff('olddir/oldfile', 'newdir/newfile')
838
self.assertNotContainsRe(
839
differ.to_file.getvalue(),
840
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
841
br' \@\@\n-old\n\+new\n\n')
842
self.assertContainsRe(differ.to_file.getvalue(),
843
b'was: old\nis: new\n')
845
def test_extra_factories(self):
846
self.create_old_new()
847
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
848
extra_factories=[DiffWasIs.from_diff_tree])
849
differ.diff('olddir/oldfile', 'newdir/newfile')
850
self.assertNotContainsRe(
851
differ.to_file.getvalue(),
852
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
853
br' \@\@\n-old\n\+new\n\n')
854
self.assertContainsRe(differ.to_file.getvalue(),
855
b'was: old\nis: new\n')
857
def test_alphabetical_order(self):
858
self.build_tree(['new-tree/a-file'])
859
self.new_tree.add('a-file')
860
self.build_tree(['old-tree/b-file'])
861
self.old_tree.add('b-file')
862
self.differ.show_diff(None)
863
self.assertContainsRe(self.differ.to_file.getvalue(),
864
b'.*a-file(.|\n)*b-file')
867
class TestDiffFromTool(tests.TestCaseWithTransport):
869
def test_from_string(self):
870
diff_obj = diff.DiffFromTool.from_string(
871
['diff', '{old_path}', '{new_path}'],
873
self.addCleanup(diff_obj.finish)
874
self.assertEqual(['diff', '{old_path}', '{new_path}'],
875
diff_obj.command_template)
877
def test_from_string_u5(self):
878
diff_obj = diff.DiffFromTool.from_string(
879
['diff', "-u 5", '{old_path}', '{new_path}'], None, None, None)
880
self.addCleanup(diff_obj.finish)
881
self.assertEqual(['diff', '-u 5', '{old_path}', '{new_path}'],
882
diff_obj.command_template)
883
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
884
diff_obj._get_command('old-path', 'new-path'))
886
def test_from_string_path_with_backslashes(self):
887
self.requireFeature(features.backslashdir_feature)
888
tool = ['C:\\Tools\\Diff.exe', '{old_path}', '{new_path}']
889
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
890
self.addCleanup(diff_obj.finish)
891
self.assertEqual(['C:\\Tools\\Diff.exe', '{old_path}', '{new_path}'],
892
diff_obj.command_template)
893
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
894
diff_obj._get_command('old-path', 'new-path'))
896
def test_execute(self):
898
diff_obj = diff.DiffFromTool([sys.executable, '-c',
899
'print("{old_path} {new_path}")'],
901
self.addCleanup(diff_obj.finish)
902
diff_obj._execute('old', 'new')
903
self.assertEqual(output.getvalue().rstrip(), b'old new')
905
def test_execute_missing(self):
906
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
908
self.addCleanup(diff_obj.finish)
909
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
911
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
912
' on this machine', str(e))
914
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
915
self.requireFeature(features.AttribFeature)
917
tree = self.make_branch_and_tree('tree')
918
self.build_tree_contents([('tree/file', b'content')])
919
tree.add('file', b'file-id')
920
tree.commit('old tree')
922
self.addCleanup(tree.unlock)
923
basis_tree = tree.basis_tree()
924
basis_tree.lock_read()
925
self.addCleanup(basis_tree.unlock)
926
diff_obj = diff.DiffFromTool([sys.executable, '-c',
927
'print "{old_path} {new_path}"'],
928
basis_tree, tree, output)
929
diff_obj._prepare_files('file', 'file', file_id=b'file-id')
930
# The old content should be readonly
931
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
933
# The new content should use the tree object, not a 'new' file anymore
934
self.assertEndsWith(tree.basedir, 'work/tree')
935
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
937
def assertReadableByAttrib(self, cwd, relpath, regex):
938
proc = subprocess.Popen(['attrib', relpath],
939
stdout=subprocess.PIPE,
941
(result, err) = proc.communicate()
942
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
944
def test_prepare_files(self):
946
tree = self.make_branch_and_tree('tree')
947
self.build_tree_contents([('tree/oldname', b'oldcontent')])
948
self.build_tree_contents([('tree/oldname2', b'oldcontent2')])
949
tree.add('oldname', b'file-id')
950
tree.add('oldname2', b'file2-id')
951
# Earliest allowable date on FAT32 filesystems is 1980-01-01
952
tree.commit('old tree', timestamp=315532800)
953
tree.rename_one('oldname', 'newname')
954
tree.rename_one('oldname2', 'newname2')
955
self.build_tree_contents([('tree/newname', b'newcontent')])
956
self.build_tree_contents([('tree/newname2', b'newcontent2')])
957
old_tree = tree.basis_tree()
959
self.addCleanup(old_tree.unlock)
961
self.addCleanup(tree.unlock)
962
diff_obj = diff.DiffFromTool([sys.executable, '-c',
963
'print "{old_path} {new_path}"'],
964
old_tree, tree, output)
965
self.addCleanup(diff_obj.finish)
966
self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
967
old_path, new_path = diff_obj._prepare_files(
968
'oldname', 'newname')
969
self.assertContainsRe(old_path, 'old/oldname$')
970
self.assertEqual(315532800, os.stat(old_path).st_mtime)
971
self.assertContainsRe(new_path, 'tree/newname$')
972
self.assertFileEqual(b'oldcontent', old_path)
973
self.assertFileEqual(b'newcontent', new_path)
974
if osutils.host_os_dereferences_symlinks():
975
self.assertTrue(os.path.samefile('tree/newname', new_path))
976
# make sure we can create files with the same parent directories
977
diff_obj._prepare_files('oldname2', 'newname2')
980
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
982
def test_encodable_filename(self):
983
# Just checks file path for external diff tool.
984
# We cannot change CPython's internal encoding used by os.exec*.
985
diffobj = diff.DiffFromTool(['dummy', '{old_path}', '{new_path}'],
987
for _, scenario in EncodingAdapter.encoding_scenarios:
988
encoding = scenario['encoding']
989
dirname = scenario['info']['directory']
990
filename = scenario['info']['filename']
992
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
993
relpath = dirname + u'/' + filename
994
fullpath = diffobj._safe_filename('safe', relpath)
995
self.assertEqual(fullpath,
996
fullpath.encode(encoding).decode(encoding))
997
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
999
def test_unencodable_filename(self):
1000
diffobj = diff.DiffFromTool(['dummy', '{old_path}', '{new_path}'],
1002
for _, scenario in EncodingAdapter.encoding_scenarios:
1003
encoding = scenario['encoding']
1004
dirname = scenario['info']['directory']
1005
filename = scenario['info']['filename']
1007
if encoding == 'iso-8859-1':
1008
encoding = 'iso-8859-2'
1010
encoding = 'iso-8859-1'
1012
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1013
relpath = dirname + u'/' + filename
1014
fullpath = diffobj._safe_filename('safe', relpath)
1015
self.assertEqual(fullpath,
1016
fullpath.encode(encoding).decode(encoding))
1017
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1020
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1022
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1023
"""Call get_trees_and_branches_to_diff_locked."""
1024
return diff.get_trees_and_branches_to_diff_locked(
1025
path_list, revision_specs, old_url, new_url, self.addCleanup)
1027
def test_basic(self):
1028
tree = self.make_branch_and_tree('tree')
1029
(old_tree, new_tree,
1030
old_branch, new_branch,
1031
specific_files, extra_trees) = self.call_gtabtd(
1032
['tree'], None, None, None)
1034
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1035
self.assertEqual(_mod_revision.NULL_REVISION,
1036
old_tree.get_revision_id())
1037
self.assertEqual(tree.basedir, new_tree.basedir)
1038
self.assertEqual(tree.branch.base, old_branch.base)
1039
self.assertEqual(tree.branch.base, new_branch.base)
1040
self.assertIs(None, specific_files)
1041
self.assertIs(None, extra_trees)
1043
def test_with_rev_specs(self):
1044
tree = self.make_branch_and_tree('tree')
1045
self.build_tree_contents([('tree/file', b'oldcontent')])
1046
tree.add('file', b'file-id')
1047
tree.commit('old tree', timestamp=0, rev_id=b"old-id")
1048
self.build_tree_contents([('tree/file', b'newcontent')])
1049
tree.commit('new tree', timestamp=0, rev_id=b"new-id")
1051
revisions = [revisionspec.RevisionSpec.from_string('1'),
1052
revisionspec.RevisionSpec.from_string('2')]
1053
(old_tree, new_tree,
1054
old_branch, new_branch,
1055
specific_files, extra_trees) = self.call_gtabtd(
1056
['tree'], revisions, None, None)
1058
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1059
self.assertEqual(b"old-id", old_tree.get_revision_id())
1060
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1061
self.assertEqual(b"new-id", new_tree.get_revision_id())
1062
self.assertEqual(tree.branch.base, old_branch.base)
1063
self.assertEqual(tree.branch.base, new_branch.base)
1064
self.assertIs(None, specific_files)
1065
self.assertEqual(tree.basedir, extra_trees[0].basedir)