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
18
from io import BytesIO
29
revision as _mod_revision,
38
from ..tests.scenarios import load_tests_apply_scenarios
41
load_tests = load_tests_apply_scenarios
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)
50
def udiff_lines(old, new, allow_binary=False):
52
diff.internal_diff('old', old, 'new', new, output, allow_binary)
54
return output.readlines()
57
def external_udiff_lines(old, new, use_stringio=False):
59
# BytesIO has no fileno, so it tests a different codepath
62
output = tempfile.TemporaryFile()
64
diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
66
raise tests.TestSkipped('external "diff" not present to test')
68
lines = output.readlines()
74
"""Simple file-like object that allows writes with any type and records."""
77
self.write_record = []
79
def write(self, data):
80
self.write_record.append(data)
82
def check_types(self, testcase, expected_type):
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))
89
class TestDiffOptions(tests.TestCase):
91
def test_unified_added(self):
92
"""Check for default style '-u' only if no other style specified
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']))
100
class TestDiffOptionsScenarios(tests.TestCase):
102
scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
103
style = None # Set by load_tests_apply_scenarios from scenarios
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)
112
class TestDiff(tests.TestCase):
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]
121
def test_add_nl_2(self):
122
"""diff generates a valid diff for patches that change last line and
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]
130
def test_remove_nl(self):
131
"""diff generates a valid diff for patches that change last line and
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]
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)
153
def test_binary_lines(self):
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)
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])
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,
172
self.check_patch(lines)
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'])
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'])
193
def test_internal_diff_default(self):
194
# Default internal diff encoding is utf8
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',
208
def test_internal_diff_utf8(self):
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',
223
def test_internal_diff_iso_8859_1(self):
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',
232
b'@@ -1,1 +1,1 @@\n',
238
def test_internal_diff_no_content(self):
240
diff.internal_diff(u'old', [], u'new', [], output)
241
self.assertEqual(b'', output.getvalue())
243
def test_internal_diff_no_changes(self):
245
diff.internal_diff(u'old', [b'text\n', b'contents\n'],
246
u'new', [b'text\n', b'contents\n'],
248
self.assertEqual(b'', output.getvalue())
250
def test_internal_diff_returns_bytes(self):
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)
256
def test_internal_diff_default_context(self):
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',
266
b'@@ -3,4 +3,4 @@\n',
275
def test_internal_diff_no_context(self):
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,
282
lines = output.getvalue().splitlines(True)
283
self.check_patch(lines)
284
self.assertEqual([b'--- old\n',
286
b'@@ -6,1 +6,1 @@\n',
292
def test_internal_diff_more_context(self):
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,
299
lines = output.getvalue().splitlines(True)
300
self.check_patch(lines)
301
self.assertEqual([b'--- old\n',
303
b'@@ -2,5 +2,5 @@\n',
314
class TestDiffFiles(tests.TestCaseInTempDir):
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'])
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)
335
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
337
if working_tree is not None:
338
extra_trees = (working_tree,)
341
diff.show_diff_trees(tree1, tree2, output,
342
specific_files=specific_files,
343
extra_trees=extra_trees, old_label='old/',
345
return output.getvalue()
348
class TestDiffDates(tests.TestCaseWithTransport):
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')
358
self.wt.add(['file1', 'file2'])
360
message='Revision 1',
361
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
364
self.build_tree_contents([('file1', b'file1 contents at rev 2\n')])
366
message='Revision 2',
367
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
370
self.build_tree_contents([('file2', b'file2 contents at rev 3\n')])
372
message='Revision 3',
373
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
376
self.wt.remove(['file2'])
378
message='Revision 4',
379
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
382
self.build_tree_contents([
383
('file1', b'file1 contents in working tree\n')
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
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
397
-file1 contents at rev 2
398
+file1 contents in working tree
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
411
-file2 contents at rev 1
412
+file2 contents at rev 3
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
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
427
+file1 contents at rev 1
429
=== added file 'file2'
430
--- old/file2\t1970-01-01 00:00:00 +0000
431
+++ new/file2\t2006-04-01 00:00:00 +0000
433
+file2 contents at rev 1
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
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
448
-file2 contents at rev 3
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')
461
def test_recursive_diff(self):
462
"""Children of directories are matched"""
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')
477
class TestShowDiffTrees(tests.TestCaseWithTransport):
478
"""Direct tests for show_diff_trees"""
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')
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')
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')
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')
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')
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")
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')
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
541
self.assertNotContainsRe(d, b'---')
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')
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')
559
def test_internal_diff_exec_property(self):
560
tree = self.make_branch_and_tree('tree')
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)
570
tree.commit('one', rev_id=b'rev-1')
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'))
578
tree.rename_one('c', 'new-c')
579
tree.rename_one('d', 'new-d')
581
d = get_diff_as_string(tree.basis_tree(), tree)
583
self.assertContainsRe(d, br"file 'a'.*\(properties changed:"
585
self.assertContainsRe(d, br"file 'b'.*\(properties changed:"
587
self.assertContainsRe(d, br"file 'c'.*\(properties changed:"
589
self.assertContainsRe(d, br"file 'd'.*\(properties changed:"
591
self.assertNotContainsRe(d, br"file 'e'")
592
self.assertNotContainsRe(d, br"file 'f'")
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.
598
# See https://bugs.launchpad.net/bugs/110092.
599
self.requireFeature(features.UnicodeFilenameFeature)
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'),
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,))
621
def test_unicode_filename(self):
622
"""Test when the filename are unicode."""
623
self.requireFeature(features.UnicodeFilenameFeature)
625
alpha, omega = u'\u03b1', u'\u03c9'
626
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
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'])
636
tree.commit('one', rev_id=b'rev-1')
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')])
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)
651
def test_unicode_filename_path_encoding(self):
652
"""Test for bug #382699: unicode filenames on Windows should be shown
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'
662
tree = self.make_branch_and_tree('.')
663
self.build_tree_contents([
664
(test_txt, b'foo\n'),
668
tree.add([test_txt, u1234, directory])
671
diff.show_diff_trees(tree.basis_tree(), tree, sio,
672
path_encoding='cp1251')
674
output = subst_dates(sio.getvalue())
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
683
=== added file '?.txt'
684
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
685
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
689
''' % {b'directory': _russian_test.encode('cp1251'),
690
b'test_txt': test_txt.encode('cp1251'),
692
self.assertEqualDiff(output, shouldbe)
695
class DiffWasIs(diff.DiffPath):
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())
704
class TestDiffTree(tests.TestCaseWithTransport):
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())
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')
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')
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')
740
b'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
741
differ.to_file.getvalue())
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')
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')
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())
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())
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())
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')
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'
807
self.assertContainsRe(self.differ.to_file.getvalue(),
808
b"=== target is 'new'\n")
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'')
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')
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)
832
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
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')
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')
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')
865
class TestDiffFromTool(tests.TestCaseWithTransport):
867
def test_from_string(self):
868
diff_obj = diff.DiffFromTool.from_string(
869
['diff', '{old_path}', '{new_path}'],
871
self.addCleanup(diff_obj.finish)
872
self.assertEqual(['diff', '{old_path}', '{new_path}'],
873
diff_obj.command_template)
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'))
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'))
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'))
903
def test_execute(self):
905
diff_obj = diff.DiffFromTool([sys.executable, '-c',
906
'print("{old_path} {new_path}")'],
908
self.addCleanup(diff_obj.finish)
909
diff_obj._execute('old', 'new')
910
self.assertEqual(output.getvalue().rstrip(), b'old new')
912
def test_execute_missing(self):
913
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
915
self.addCleanup(diff_obj.finish)
916
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
918
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
919
' on this machine', str(e))
921
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
922
self.requireFeature(features.AttribFeature)
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')
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',
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$')
944
def assertReadableByAttrib(self, cwd, relpath, regex):
945
proc = subprocess.Popen(['attrib', relpath],
946
stdout=subprocess.PIPE,
948
(result, err) = proc.communicate()
949
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
951
def test_prepare_files(self):
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()
966
self.addCleanup(old_tree.unlock)
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')
987
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
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}'],
994
for _, scenario in EncodingAdapter.encoding_scenarios:
995
encoding = scenario['encoding']
996
dirname = scenario['info']['directory']
997
filename = scenario['info']['filename']
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'))
1006
def test_unencodable_filename(self):
1007
diffobj = diff.DiffFromTool(['dummy', '{old_path}', '{new_path}'],
1009
for _, scenario in EncodingAdapter.encoding_scenarios:
1010
encoding = scenario['encoding']
1011
dirname = scenario['info']['directory']
1012
filename = scenario['info']['filename']
1014
if encoding == 'iso-8859-1':
1015
encoding = 'iso-8859-2'
1017
encoding = 'iso-8859-1'
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'))
1027
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
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)
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)
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)
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")
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)
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)