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
28
revision as _mod_revision,
33
from ..sixish import (
41
from ..tests.scenarios import load_tests_apply_scenarios
44
load_tests = load_tests_apply_scenarios
47
def subst_dates(string):
48
"""Replace date strings with constant values."""
49
return re.sub(br'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [-\+]\d{4}',
50
b'YYYY-MM-DD HH:MM:SS +ZZZZ', string)
53
def udiff_lines(old, new, allow_binary=False):
55
diff.internal_diff('old', old, 'new', new, output, allow_binary)
1
from bzrlib.selftest 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)
57
8
return output.readlines()
60
def external_udiff_lines(old, new, use_stringio=False):
62
# BytesIO has no fileno, so it tests a different codepath
65
output = tempfile.TemporaryFile()
67
diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
69
raise tests.TestSkipped('external "diff" not present to test')
71
lines = output.readlines()
77
"""Simple file-like object that allows writes with any type and records."""
80
self.write_record = []
82
def write(self, data):
83
self.write_record.append(data)
85
def check_types(self, testcase, expected_type):
87
any(not isinstance(o, expected_type) for o in self.write_record),
88
"Not all writes of type %s: %r" % (
89
expected_type.__name__, self.write_record))
92
class TestDiffOptions(tests.TestCase):
94
def test_unified_added(self):
95
"""Check for default style '-u' only if no other style specified
98
# Verify that style defaults to unified, id est '-u' appended
99
# to option list, in the absence of an alternative style.
100
self.assertEqual(['-a', '-u'], diff.default_style_unified(['-a']))
103
class TestDiffOptionsScenarios(tests.TestCase):
105
scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
106
style = None # Set by load_tests_apply_scenarios from scenarios
108
def test_unified_not_added(self):
109
# Verify that for all valid style options, '-u' is not
110
# appended to option list.
111
ret_opts = diff.default_style_unified(diff_opts=["%s" % (self.style,)])
112
self.assertEqual(["%s" % (self.style,)], ret_opts)
115
class TestDiff(tests.TestCase):
10
class TestDiff(TestCase):
117
11
def test_add_nl(self):
118
12
"""diff generates a valid diff for patches that add a newline"""
119
lines = udiff_lines([b'boo'], [b'boo\n'])
13
lines = udiff_lines(['boo'], ['boo\n'])
120
14
self.check_patch(lines)
121
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
122
## "expected no-nl, got %r" % lines[4]
15
self.assertEquals(lines[4], '\\ No newline at end of file\n')
16
## "expected no-nl, got %r" % lines[4]
124
18
def test_add_nl_2(self):
125
19
"""diff generates a valid diff for patches that change last line and
128
lines = udiff_lines([b'boo'], [b'goo\n'])
22
lines = udiff_lines(['boo'], ['goo\n'])
129
23
self.check_patch(lines)
130
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
131
## "expected no-nl, got %r" % lines[4]
24
self.assertEquals(lines[4], '\\ No newline at end of file\n')
25
## "expected no-nl, got %r" % lines[4]
133
27
def test_remove_nl(self):
134
28
"""diff generates a valid diff for patches that change last line and
137
lines = udiff_lines([b'boo\n'], [b'boo'])
31
lines = udiff_lines(['boo\n'], ['boo'])
138
32
self.check_patch(lines)
139
self.assertEqual(lines[5], b'\\ No newline at end of file\n')
140
## "expected no-nl, got %r" % lines[5]
33
self.assertEquals(lines[5], '\\ No newline at end of file\n')
34
## "expected no-nl, got %r" % lines[5]
142
36
def check_patch(self, lines):
143
self.assertTrue(len(lines) > 1)
144
## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
145
self.assertTrue(lines[0].startswith(b'---'))
146
## 'No orig line for patch:\n%s' % "".join(lines)
147
self.assertTrue(lines[1].startswith(b'+++'))
148
## 'No mod line for patch:\n%s' % "".join(lines)
149
self.assertTrue(len(lines) > 2)
150
## "No hunks for patch:\n%s" % "".join(lines)
151
self.assertTrue(lines[2].startswith(b'@@'))
152
## "No hunk header for patch:\n%s" % "".join(lines)
153
self.assertTrue(b'@@' in lines[2][2:])
154
## "Unterminated hunk header for patch:\n%s" % "".join(lines)
156
def test_binary_lines(self):
158
uni_lines = [1023 * b'a' + b'\x00']
159
self.assertRaises(errors.BinaryFile, udiff_lines, uni_lines, empty)
160
self.assertRaises(errors.BinaryFile, udiff_lines, empty, uni_lines)
161
udiff_lines(uni_lines, empty, allow_binary=True)
162
udiff_lines(empty, uni_lines, allow_binary=True)
164
def test_external_diff(self):
165
lines = external_udiff_lines([b'boo\n'], [b'goo\n'])
166
self.check_patch(lines)
167
self.assertEqual(b'\n', lines[-1])
169
def test_external_diff_no_fileno(self):
170
# Make sure that we can handle not having a fileno, even
171
# if the diff is large
172
lines = external_udiff_lines([b'boo\n'] * 10000,
175
self.check_patch(lines)
177
def test_external_diff_binary_lang_c(self):
178
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
179
self.overrideEnv(lang, 'C')
180
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
181
# Older versions of diffutils say "Binary files", newer
182
# versions just say "Files".
183
self.assertContainsRe(
184
lines[0], b'(Binary f|F)iles old and new differ\n')
185
self.assertEqual(lines[1:], [b'\n'])
187
def test_no_external_diff(self):
188
"""Check that NoDiff is raised when diff is not available"""
189
# Make sure no 'diff' command is available
190
# XXX: Weird, using None instead of '' breaks the test -- vila 20101216
191
self.overrideEnv('PATH', '')
192
self.assertRaises(errors.NoDiff, diff.external_diff,
193
b'old', [b'boo\n'], b'new', [b'goo\n'],
194
BytesIO(), diff_opts=['-u'])
196
def test_internal_diff_default(self):
197
# Default internal diff encoding is utf8
199
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
200
u'new_\xe5', [b'new_text\n'], output)
201
lines = output.getvalue().splitlines(True)
202
self.check_patch(lines)
203
self.assertEqual([b'--- old_\xc2\xb5\n',
204
b'+++ new_\xc3\xa5\n',
205
b'@@ -1,1 +1,1 @@\n',
211
def test_internal_diff_utf8(self):
213
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
214
u'new_\xe5', [b'new_text\n'], output,
215
path_encoding='utf8')
216
lines = output.getvalue().splitlines(True)
217
self.check_patch(lines)
218
self.assertEqual([b'--- old_\xc2\xb5\n',
219
b'+++ new_\xc3\xa5\n',
220
b'@@ -1,1 +1,1 @@\n',
226
def test_internal_diff_iso_8859_1(self):
228
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
229
u'new_\xe5', [b'new_text\n'], output,
230
path_encoding='iso-8859-1')
231
lines = output.getvalue().splitlines(True)
232
self.check_patch(lines)
233
self.assertEqual([b'--- old_\xb5\n',
235
b'@@ -1,1 +1,1 @@\n',
241
def test_internal_diff_no_content(self):
243
diff.internal_diff(u'old', [], u'new', [], output)
244
self.assertEqual(b'', output.getvalue())
246
def test_internal_diff_no_changes(self):
248
diff.internal_diff(u'old', [b'text\n', b'contents\n'],
249
u'new', [b'text\n', b'contents\n'],
251
self.assertEqual(b'', output.getvalue())
253
def test_internal_diff_returns_bytes(self):
255
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
256
u'new_\xe5', [b'new_text\n'], output)
257
output.check_types(self, bytes)
259
def test_internal_diff_default_context(self):
261
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
262
b'same_text\n', b'same_text\n', b'old_text\n'],
263
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
264
b'same_text\n', b'same_text\n', b'new_text\n'], output)
265
lines = output.getvalue().splitlines(True)
266
self.check_patch(lines)
267
self.assertEqual([b'--- old\n',
269
b'@@ -3,4 +3,4 @@\n',
278
def test_internal_diff_no_context(self):
280
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
281
b'same_text\n', b'same_text\n', b'old_text\n'],
282
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
283
b'same_text\n', b'same_text\n', b'new_text\n'], output,
285
lines = output.getvalue().splitlines(True)
286
self.check_patch(lines)
287
self.assertEqual([b'--- old\n',
289
b'@@ -6,1 +6,1 @@\n',
295
def test_internal_diff_more_context(self):
297
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
298
b'same_text\n', b'same_text\n', b'old_text\n'],
299
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
300
b'same_text\n', b'same_text\n', b'new_text\n'], output,
302
lines = output.getvalue().splitlines(True)
303
self.check_patch(lines)
304
self.assertEqual([b'--- old\n',
306
b'@@ -2,5 +2,5 @@\n',
317
class TestDiffFiles(tests.TestCaseInTempDir):
319
def test_external_diff_binary(self):
320
"""The output when using external diff should use diff's i18n error"""
321
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
322
self.overrideEnv(lang, 'C')
323
# Make sure external_diff doesn't fail in the current LANG
324
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
326
cmd = ['diff', '-u', '--binary', 'old', 'new']
327
with open('old', 'wb') as f:
328
f.write(b'\x00foobar\n')
329
with open('new', 'wb') as f:
330
f.write(b'foo\x00bar\n')
331
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
332
stdin=subprocess.PIPE)
333
out, err = pipe.communicate()
334
# We should output whatever diff tells us, plus a trailing newline
335
self.assertEqual(out.splitlines(True) + [b'\n'], lines)
338
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
340
if working_tree is not None:
341
extra_trees = (working_tree,)
344
diff.show_diff_trees(tree1, tree2, output,
345
specific_files=specific_files,
346
extra_trees=extra_trees, old_label='old/',
348
return output.getvalue()
351
class TestDiffDates(tests.TestCaseWithTransport):
354
super(TestDiffDates, self).setUp()
355
self.wt = self.make_branch_and_tree('.')
356
self.b = self.wt.branch
357
self.build_tree_contents([
358
('file1', b'file1 contents at rev 1\n'),
359
('file2', b'file2 contents at rev 1\n')
361
self.wt.add(['file1', 'file2'])
363
message='Revision 1',
364
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
367
self.build_tree_contents([('file1', b'file1 contents at rev 2\n')])
369
message='Revision 2',
370
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
373
self.build_tree_contents([('file2', b'file2 contents at rev 3\n')])
375
message='Revision 3',
376
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
379
self.wt.remove(['file2'])
381
message='Revision 4',
382
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
385
self.build_tree_contents([
386
('file1', b'file1 contents in working tree\n')
388
# set the date stamps for files in the working tree to known values
389
os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
391
def test_diff_rev_tree_working_tree(self):
392
output = get_diff_as_string(self.wt.basis_tree(), self.wt)
393
# note that the date for old/file1 is from rev 2 rather than from
394
# the basis revision (rev 4)
395
self.assertEqualDiff(output, b'''\
396
=== modified file 'file1'
397
--- old/file1\t2006-04-02 00:00:00 +0000
398
+++ new/file1\t2006-04-05 00:00:00 +0000
400
-file1 contents at rev 2
401
+file1 contents in working tree
405
def test_diff_rev_tree_rev_tree(self):
406
tree1 = self.b.repository.revision_tree(b'rev-2')
407
tree2 = self.b.repository.revision_tree(b'rev-3')
408
output = get_diff_as_string(tree1, tree2)
409
self.assertEqualDiff(output, b'''\
410
=== modified file 'file2'
411
--- old/file2\t2006-04-01 00:00:00 +0000
412
+++ new/file2\t2006-04-03 00:00:00 +0000
414
-file2 contents at rev 1
415
+file2 contents at rev 3
419
def test_diff_add_files(self):
420
tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
421
tree2 = self.b.repository.revision_tree(b'rev-1')
422
output = get_diff_as_string(tree1, tree2)
423
# the files have the epoch time stamp for the tree in which
425
self.assertEqualDiff(output, b'''\
426
=== added file 'file1'
427
--- old/file1\t1970-01-01 00:00:00 +0000
428
+++ new/file1\t2006-04-01 00:00:00 +0000
430
+file1 contents at rev 1
432
=== added file 'file2'
433
--- old/file2\t1970-01-01 00:00:00 +0000
434
+++ new/file2\t2006-04-01 00:00:00 +0000
436
+file2 contents at rev 1
440
def test_diff_remove_files(self):
441
tree1 = self.b.repository.revision_tree(b'rev-3')
442
tree2 = self.b.repository.revision_tree(b'rev-4')
443
output = get_diff_as_string(tree1, tree2)
444
# the file has the epoch time stamp for the tree in which
446
self.assertEqualDiff(output, b'''\
447
=== removed file 'file2'
448
--- old/file2\t2006-04-03 00:00:00 +0000
449
+++ new/file2\t1970-01-01 00:00:00 +0000
451
-file2 contents at rev 3
455
def test_show_diff_specified(self):
456
"""A working tree filename can be used to identify a file"""
457
self.wt.rename_one('file1', 'file1b')
458
old_tree = self.b.repository.revision_tree(b'rev-1')
459
new_tree = self.b.repository.revision_tree(b'rev-4')
460
out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
461
working_tree=self.wt)
462
self.assertContainsRe(out, b'file1\t')
464
def test_recursive_diff(self):
465
"""Children of directories are matched"""
468
self.wt.add(['dir1', 'dir2'])
469
self.wt.rename_one('file1', 'dir1/file1')
470
old_tree = self.b.repository.revision_tree(b'rev-1')
471
new_tree = self.b.repository.revision_tree(b'rev-4')
472
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
473
working_tree=self.wt)
474
self.assertContainsRe(out, b'file1\t')
475
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
476
working_tree=self.wt)
477
self.assertNotContainsRe(out, b'file1\t')
480
class TestShowDiffTrees(tests.TestCaseWithTransport):
481
"""Direct tests for show_diff_trees"""
483
def test_modified_file(self):
484
"""Test when a file is modified."""
485
tree = self.make_branch_and_tree('tree')
486
self.build_tree_contents([('tree/file', b'contents\n')])
487
tree.add(['file'], [b'file-id'])
488
tree.commit('one', rev_id=b'rev-1')
490
self.build_tree_contents([('tree/file', b'new contents\n')])
491
d = get_diff_as_string(tree.basis_tree(), tree)
492
self.assertContainsRe(d, b"=== modified file 'file'\n")
493
self.assertContainsRe(d, b'--- old/file\t')
494
self.assertContainsRe(d, b'\\+\\+\\+ new/file\t')
495
self.assertContainsRe(d, b'-contents\n'
496
b'\\+new contents\n')
498
def test_modified_file_in_renamed_dir(self):
499
"""Test when a file is modified in a renamed directory."""
500
tree = self.make_branch_and_tree('tree')
501
self.build_tree(['tree/dir/'])
502
self.build_tree_contents([('tree/dir/file', b'contents\n')])
503
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
504
tree.commit('one', rev_id=b'rev-1')
506
tree.rename_one('dir', 'other')
507
self.build_tree_contents([('tree/other/file', b'new contents\n')])
508
d = get_diff_as_string(tree.basis_tree(), tree)
509
self.assertContainsRe(d, b"=== renamed directory 'dir' => 'other'\n")
510
self.assertContainsRe(d, b"=== modified file 'other/file'\n")
511
# XXX: This is technically incorrect, because it used to be at another
512
# location. What to do?
513
self.assertContainsRe(d, b'--- old/dir/file\t')
514
self.assertContainsRe(d, b'\\+\\+\\+ new/other/file\t')
515
self.assertContainsRe(d, b'-contents\n'
516
b'\\+new contents\n')
518
def test_renamed_directory(self):
519
"""Test when only a directory is only renamed."""
520
tree = self.make_branch_and_tree('tree')
521
self.build_tree(['tree/dir/'])
522
self.build_tree_contents([('tree/dir/file', b'contents\n')])
523
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
524
tree.commit('one', rev_id=b'rev-1')
526
tree.rename_one('dir', 'newdir')
527
d = get_diff_as_string(tree.basis_tree(), tree)
528
# Renaming a directory should be a single "you renamed this dir" even
529
# when there are files inside.
530
self.assertEqual(d, b"=== renamed directory 'dir' => 'newdir'\n")
532
def test_renamed_file(self):
533
"""Test when a file is only renamed."""
534
tree = self.make_branch_and_tree('tree')
535
self.build_tree_contents([('tree/file', b'contents\n')])
536
tree.add(['file'], [b'file-id'])
537
tree.commit('one', rev_id=b'rev-1')
539
tree.rename_one('file', 'newname')
540
d = get_diff_as_string(tree.basis_tree(), tree)
541
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
542
# We shouldn't have a --- or +++ line, because there is no content
544
self.assertNotContainsRe(d, b'---')
546
def test_renamed_and_modified_file(self):
547
"""Test when a file is only renamed."""
548
tree = self.make_branch_and_tree('tree')
549
self.build_tree_contents([('tree/file', b'contents\n')])
550
tree.add(['file'], [b'file-id'])
551
tree.commit('one', rev_id=b'rev-1')
553
tree.rename_one('file', 'newname')
554
self.build_tree_contents([('tree/newname', b'new contents\n')])
555
d = get_diff_as_string(tree.basis_tree(), tree)
556
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
557
self.assertContainsRe(d, b'--- old/file\t')
558
self.assertContainsRe(d, b'\\+\\+\\+ new/newname\t')
559
self.assertContainsRe(d, b'-contents\n'
560
b'\\+new contents\n')
562
def test_internal_diff_exec_property(self):
563
tree = self.make_branch_and_tree('tree')
565
tt = tree.get_transform()
566
tt.new_file('a', tt.root, [b'contents\n'], b'a-id', True)
567
tt.new_file('b', tt.root, [b'contents\n'], b'b-id', False)
568
tt.new_file('c', tt.root, [b'contents\n'], b'c-id', True)
569
tt.new_file('d', tt.root, [b'contents\n'], b'd-id', False)
570
tt.new_file('e', tt.root, [b'contents\n'], b'control-e-id', True)
571
tt.new_file('f', tt.root, [b'contents\n'], b'control-f-id', False)
573
tree.commit('one', rev_id=b'rev-1')
575
tt = tree.get_transform()
576
tt.set_executability(False, tt.trans_id_file_id(b'a-id'))
577
tt.set_executability(True, tt.trans_id_file_id(b'b-id'))
578
tt.set_executability(False, tt.trans_id_file_id(b'c-id'))
579
tt.set_executability(True, tt.trans_id_file_id(b'd-id'))
581
tree.rename_one('c', 'new-c')
582
tree.rename_one('d', 'new-d')
584
d = get_diff_as_string(tree.basis_tree(), tree)
586
self.assertContainsRe(d, br"file 'a'.*\(properties changed:"
588
self.assertContainsRe(d, br"file 'b'.*\(properties changed:"
590
self.assertContainsRe(d, br"file 'c'.*\(properties changed:"
592
self.assertContainsRe(d, br"file 'd'.*\(properties changed:"
594
self.assertNotContainsRe(d, br"file 'e'")
595
self.assertNotContainsRe(d, br"file 'f'")
597
def test_binary_unicode_filenames(self):
598
"""Test that contents of files are *not* encoded in UTF-8 when there
599
is a binary file in the diff.
601
# See https://bugs.launchpad.net/bugs/110092.
602
self.requireFeature(features.UnicodeFilenameFeature)
604
tree = self.make_branch_and_tree('tree')
605
alpha, omega = u'\u03b1', u'\u03c9'
606
alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
607
self.build_tree_contents(
608
[('tree/' + alpha, b'\0'),
610
(b'The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
611
tree.add([alpha], [b'file-id'])
612
tree.add([omega], [b'file-id-2'])
613
diff_content = StubO()
614
diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
615
diff_content.check_types(self, bytes)
616
d = b''.join(diff_content.write_record)
617
self.assertContainsRe(d, br"=== added file '%s'" % alpha_utf8)
618
self.assertContainsRe(d, b"Binary files a/%s.*and b/%s.* differ\n"
619
% (alpha_utf8, alpha_utf8))
620
self.assertContainsRe(d, br"=== added file '%s'" % omega_utf8)
621
self.assertContainsRe(d, br"--- a/%s" % (omega_utf8,))
622
self.assertContainsRe(d, br"\+\+\+ b/%s" % (omega_utf8,))
624
def test_unicode_filename(self):
625
"""Test when the filename are unicode."""
626
self.requireFeature(features.UnicodeFilenameFeature)
628
alpha, omega = u'\u03b1', u'\u03c9'
629
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
631
tree = self.make_branch_and_tree('tree')
632
self.build_tree_contents([('tree/ren_' + alpha, b'contents\n')])
633
tree.add(['ren_' + alpha], [b'file-id-2'])
634
self.build_tree_contents([('tree/del_' + alpha, b'contents\n')])
635
tree.add(['del_' + alpha], [b'file-id-3'])
636
self.build_tree_contents([('tree/mod_' + alpha, b'contents\n')])
637
tree.add(['mod_' + alpha], [b'file-id-4'])
639
tree.commit('one', rev_id=b'rev-1')
641
tree.rename_one('ren_' + alpha, 'ren_' + omega)
642
tree.remove('del_' + alpha)
643
self.build_tree_contents([('tree/add_' + alpha, b'contents\n')])
644
tree.add(['add_' + alpha], [b'file-id'])
645
self.build_tree_contents([('tree/mod_' + alpha, b'contents_mod\n')])
647
d = get_diff_as_string(tree.basis_tree(), tree)
648
self.assertContainsRe(d,
649
b"=== renamed file 'ren_%s' => 'ren_%s'\n" % (autf8, outf8))
650
self.assertContainsRe(d, b"=== added file 'add_%s'" % autf8)
651
self.assertContainsRe(d, b"=== modified file 'mod_%s'" % autf8)
652
self.assertContainsRe(d, b"=== removed file 'del_%s'" % autf8)
654
def test_unicode_filename_path_encoding(self):
655
"""Test for bug #382699: unicode filenames on Windows should be shown
658
self.requireFeature(features.UnicodeFilenameFeature)
659
# The word 'test' in Russian
660
_russian_test = u'\u0422\u0435\u0441\u0442'
661
directory = _russian_test + u'/'
662
test_txt = _russian_test + u'.txt'
663
u1234 = u'\u1234.txt'
665
tree = self.make_branch_and_tree('.')
666
self.build_tree_contents([
667
(test_txt, b'foo\n'),
671
tree.add([test_txt, u1234, directory])
674
diff.show_diff_trees(tree.basis_tree(), tree, sio,
675
path_encoding='cp1251')
677
output = subst_dates(sio.getvalue())
679
=== added directory '%(directory)s'
680
=== added file '%(test_txt)s'
681
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
682
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
686
=== added file '?.txt'
687
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
688
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
692
''' % {b'directory': _russian_test.encode('cp1251'),
693
b'test_txt': test_txt.encode('cp1251'),
695
self.assertEqualDiff(output, shouldbe)
698
class DiffWasIs(diff.DiffPath):
700
def diff(self, old_path, new_path, old_kind, new_kind):
701
self.to_file.write(b'was: ')
702
self.to_file.write(self.old_tree.get_file(old_path).read())
703
self.to_file.write(b'is: ')
704
self.to_file.write(self.new_tree.get_file(new_path).read())
707
class TestDiffTree(tests.TestCaseWithTransport):
710
super(TestDiffTree, self).setUp()
711
self.old_tree = self.make_branch_and_tree('old-tree')
712
self.old_tree.lock_write()
713
self.addCleanup(self.old_tree.unlock)
714
self.new_tree = self.make_branch_and_tree('new-tree')
715
self.new_tree.lock_write()
716
self.addCleanup(self.new_tree.unlock)
717
self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
719
def test_diff_text(self):
720
self.build_tree_contents([('old-tree/olddir/',),
721
('old-tree/olddir/oldfile', b'old\n')])
722
self.old_tree.add('olddir')
723
self.old_tree.add('olddir/oldfile', b'file-id')
724
self.build_tree_contents([('new-tree/newdir/',),
725
('new-tree/newdir/newfile', b'new\n')])
726
self.new_tree.add('newdir')
727
self.new_tree.add('newdir/newfile', b'file-id')
728
differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
729
differ.diff_text('olddir/oldfile', None, 'old label', 'new label')
731
b'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
732
differ.to_file.getvalue())
733
differ.to_file.seek(0)
734
differ.diff_text(None, 'newdir/newfile',
735
'old label', 'new label')
737
b'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
738
differ.to_file.getvalue())
739
differ.to_file.seek(0)
740
differ.diff_text('olddir/oldfile', 'newdir/newfile',
741
'old label', 'new label')
743
b'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
744
differ.to_file.getvalue())
746
def test_diff_deletion(self):
747
self.build_tree_contents([('old-tree/file', b'contents'),
748
('new-tree/file', b'contents')])
749
self.old_tree.add('file', b'file-id')
750
self.new_tree.add('file', b'file-id')
751
os.unlink('new-tree/file')
752
self.differ.show_diff(None)
753
self.assertContainsRe(self.differ.to_file.getvalue(), b'-contents')
755
def test_diff_creation(self):
756
self.build_tree_contents([('old-tree/file', b'contents'),
757
('new-tree/file', b'contents')])
758
self.old_tree.add('file', b'file-id')
759
self.new_tree.add('file', b'file-id')
760
os.unlink('old-tree/file')
761
self.differ.show_diff(None)
762
self.assertContainsRe(self.differ.to_file.getvalue(), br'\+contents')
764
def test_diff_symlink(self):
765
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
766
differ.diff_symlink('old target', None)
767
self.assertEqual(b"=== target was 'old target'\n",
768
differ.to_file.getvalue())
770
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
771
differ.diff_symlink(None, 'new target')
772
self.assertEqual(b"=== target is 'new target'\n",
773
differ.to_file.getvalue())
775
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
776
differ.diff_symlink('old target', 'new target')
777
self.assertEqual(b"=== target changed 'old target' => 'new target'\n",
778
differ.to_file.getvalue())
781
self.build_tree_contents([('old-tree/olddir/',),
782
('old-tree/olddir/oldfile', b'old\n')])
783
self.old_tree.add('olddir')
784
self.old_tree.add('olddir/oldfile', b'file-id')
785
self.build_tree_contents([('new-tree/newdir/',),
786
('new-tree/newdir/newfile', b'new\n')])
787
self.new_tree.add('newdir')
788
self.new_tree.add('newdir/newfile', b'file-id')
789
self.differ.diff('olddir/oldfile', 'newdir/newfile')
790
self.assertContainsRe(
791
self.differ.to_file.getvalue(),
792
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
793
br' \@\@\n-old\n\+new\n\n')
795
def test_diff_kind_change(self):
796
self.requireFeature(features.SymlinkFeature)
797
self.build_tree_contents([('old-tree/olddir/',),
798
('old-tree/olddir/oldfile', b'old\n')])
799
self.old_tree.add('olddir')
800
self.old_tree.add('olddir/oldfile', b'file-id')
801
self.build_tree(['new-tree/newdir/'])
802
os.symlink('new', 'new-tree/newdir/newfile')
803
self.new_tree.add('newdir')
804
self.new_tree.add('newdir/newfile', b'file-id')
805
self.differ.diff('olddir/oldfile', 'newdir/newfile')
806
self.assertContainsRe(
807
self.differ.to_file.getvalue(),
808
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
810
self.assertContainsRe(self.differ.to_file.getvalue(),
811
b"=== target is 'new'\n")
813
def test_diff_directory(self):
814
self.build_tree(['new-tree/new-dir/'])
815
self.new_tree.add('new-dir', b'new-dir-id')
816
self.differ.diff(None, 'new-dir')
817
self.assertEqual(self.differ.to_file.getvalue(), b'')
819
def create_old_new(self):
820
self.build_tree_contents([('old-tree/olddir/',),
821
('old-tree/olddir/oldfile', b'old\n')])
822
self.old_tree.add('olddir')
823
self.old_tree.add('olddir/oldfile', b'file-id')
824
self.build_tree_contents([('new-tree/newdir/',),
825
('new-tree/newdir/newfile', b'new\n')])
826
self.new_tree.add('newdir')
827
self.new_tree.add('newdir/newfile', b'file-id')
829
def test_register_diff(self):
830
self.create_old_new()
831
old_diff_factories = diff.DiffTree.diff_factories
832
diff.DiffTree.diff_factories = old_diff_factories[:]
833
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
835
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
837
diff.DiffTree.diff_factories = old_diff_factories
838
differ.diff('olddir/oldfile', 'newdir/newfile')
839
self.assertNotContainsRe(
840
differ.to_file.getvalue(),
841
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
842
br' \@\@\n-old\n\+new\n\n')
843
self.assertContainsRe(differ.to_file.getvalue(),
844
b'was: old\nis: new\n')
846
def test_extra_factories(self):
847
self.create_old_new()
848
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
849
extra_factories=[DiffWasIs.from_diff_tree])
850
differ.diff('olddir/oldfile', 'newdir/newfile')
851
self.assertNotContainsRe(
852
differ.to_file.getvalue(),
853
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
854
br' \@\@\n-old\n\+new\n\n')
855
self.assertContainsRe(differ.to_file.getvalue(),
856
b'was: old\nis: new\n')
858
def test_alphabetical_order(self):
859
self.build_tree(['new-tree/a-file'])
860
self.new_tree.add('a-file')
861
self.build_tree(['old-tree/b-file'])
862
self.old_tree.add('b-file')
863
self.differ.show_diff(None)
864
self.assertContainsRe(self.differ.to_file.getvalue(),
865
b'.*a-file(.|\n)*b-file')
868
class TestDiffFromTool(tests.TestCaseWithTransport):
870
def test_from_string(self):
871
diff_obj = diff.DiffFromTool.from_string(
872
['diff', '{old_path}', '{new_path}'],
874
self.addCleanup(diff_obj.finish)
875
self.assertEqual(['diff', '{old_path}', '{new_path}'],
876
diff_obj.command_template)
878
def test_from_string_no_paths(self):
879
diff_obj = diff.DiffFromTool.from_string(
880
['diff', "-u5"], None, None, None)
881
self.addCleanup(diff_obj.finish)
882
self.assertEqual(['diff', '-u5'],
883
diff_obj.command_template)
884
self.assertEqual(['diff', '-u5', 'old-path', 'new-path'],
885
diff_obj._get_command('old-path', 'new-path'))
887
def test_from_string_u5(self):
888
diff_obj = diff.DiffFromTool.from_string(
889
['diff', "-u 5", '{old_path}', '{new_path}'], None, None, None)
890
self.addCleanup(diff_obj.finish)
891
self.assertEqual(['diff', '-u 5', '{old_path}', '{new_path}'],
892
diff_obj.command_template)
893
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
894
diff_obj._get_command('old-path', 'new-path'))
896
def test_from_string_path_with_backslashes(self):
897
self.requireFeature(features.backslashdir_feature)
898
tool = ['C:\\Tools\\Diff.exe', '{old_path}', '{new_path}']
899
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
900
self.addCleanup(diff_obj.finish)
901
self.assertEqual(['C:\\Tools\\Diff.exe', '{old_path}', '{new_path}'],
902
diff_obj.command_template)
903
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
904
diff_obj._get_command('old-path', 'new-path'))
906
def test_execute(self):
908
diff_obj = diff.DiffFromTool([sys.executable, '-c',
909
'print("{old_path} {new_path}")'],
911
self.addCleanup(diff_obj.finish)
912
diff_obj._execute('old', 'new')
913
self.assertEqual(output.getvalue().rstrip(), b'old new')
915
def test_execute_missing(self):
916
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
918
self.addCleanup(diff_obj.finish)
919
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
921
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
922
' on this machine', str(e))
924
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
925
self.requireFeature(features.AttribFeature)
927
tree = self.make_branch_and_tree('tree')
928
self.build_tree_contents([('tree/file', b'content')])
929
tree.add('file', b'file-id')
930
tree.commit('old tree')
932
self.addCleanup(tree.unlock)
933
basis_tree = tree.basis_tree()
934
basis_tree.lock_read()
935
self.addCleanup(basis_tree.unlock)
936
diff_obj = diff.DiffFromTool([sys.executable, '-c',
937
'print "{old_path} {new_path}"'],
938
basis_tree, tree, output)
939
diff_obj._prepare_files('file', 'file', file_id=b'file-id')
940
# The old content should be readonly
941
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
943
# The new content should use the tree object, not a 'new' file anymore
944
self.assertEndsWith(tree.basedir, 'work/tree')
945
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
947
def assertReadableByAttrib(self, cwd, relpath, regex):
948
proc = subprocess.Popen(['attrib', relpath],
949
stdout=subprocess.PIPE,
951
(result, err) = proc.communicate()
952
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
954
def test_prepare_files(self):
956
tree = self.make_branch_and_tree('tree')
957
self.build_tree_contents([('tree/oldname', b'oldcontent')])
958
self.build_tree_contents([('tree/oldname2', b'oldcontent2')])
959
tree.add('oldname', b'file-id')
960
tree.add('oldname2', b'file2-id')
961
# Earliest allowable date on FAT32 filesystems is 1980-01-01
962
tree.commit('old tree', timestamp=315532800)
963
tree.rename_one('oldname', 'newname')
964
tree.rename_one('oldname2', 'newname2')
965
self.build_tree_contents([('tree/newname', b'newcontent')])
966
self.build_tree_contents([('tree/newname2', b'newcontent2')])
967
old_tree = tree.basis_tree()
969
self.addCleanup(old_tree.unlock)
971
self.addCleanup(tree.unlock)
972
diff_obj = diff.DiffFromTool([sys.executable, '-c',
973
'print "{old_path} {new_path}"'],
974
old_tree, tree, output)
975
self.addCleanup(diff_obj.finish)
976
self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
977
old_path, new_path = diff_obj._prepare_files(
978
'oldname', 'newname')
979
self.assertContainsRe(old_path, 'old/oldname$')
980
self.assertEqual(315532800, os.stat(old_path).st_mtime)
981
self.assertContainsRe(new_path, 'tree/newname$')
982
self.assertFileEqual(b'oldcontent', old_path)
983
self.assertFileEqual(b'newcontent', new_path)
984
if osutils.host_os_dereferences_symlinks():
985
self.assertTrue(os.path.samefile('tree/newname', new_path))
986
# make sure we can create files with the same parent directories
987
diff_obj._prepare_files('oldname2', 'newname2')
990
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
992
def test_encodable_filename(self):
993
# Just checks file path for external diff tool.
994
# We cannot change CPython's internal encoding used by os.exec*.
995
diffobj = diff.DiffFromTool(['dummy', '{old_path}', '{new_path}'],
997
for _, scenario in EncodingAdapter.encoding_scenarios:
998
encoding = scenario['encoding']
999
dirname = scenario['info']['directory']
1000
filename = scenario['info']['filename']
1002
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1003
relpath = dirname + u'/' + filename
1004
fullpath = diffobj._safe_filename('safe', relpath)
1005
self.assertEqual(fullpath,
1006
fullpath.encode(encoding).decode(encoding))
1007
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1009
def test_unencodable_filename(self):
1010
diffobj = diff.DiffFromTool(['dummy', '{old_path}', '{new_path}'],
1012
for _, scenario in EncodingAdapter.encoding_scenarios:
1013
encoding = scenario['encoding']
1014
dirname = scenario['info']['directory']
1015
filename = scenario['info']['filename']
1017
if encoding == 'iso-8859-1':
1018
encoding = 'iso-8859-2'
1020
encoding = 'iso-8859-1'
1022
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1023
relpath = dirname + u'/' + filename
1024
fullpath = diffobj._safe_filename('safe', relpath)
1025
self.assertEqual(fullpath,
1026
fullpath.encode(encoding).decode(encoding))
1027
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1030
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1032
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1033
"""Call get_trees_and_branches_to_diff_locked."""
1034
exit_stack = cleanup.ExitStack()
1035
self.addCleanup(exit_stack.close)
1036
return diff.get_trees_and_branches_to_diff_locked(
1037
path_list, revision_specs, old_url, new_url, exit_stack)
1039
def test_basic(self):
1040
tree = self.make_branch_and_tree('tree')
1041
(old_tree, new_tree,
1042
old_branch, new_branch,
1043
specific_files, extra_trees) = self.call_gtabtd(
1044
['tree'], None, None, None)
1046
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1047
self.assertEqual(_mod_revision.NULL_REVISION,
1048
old_tree.get_revision_id())
1049
self.assertEqual(tree.basedir, new_tree.basedir)
1050
self.assertEqual(tree.branch.base, old_branch.base)
1051
self.assertEqual(tree.branch.base, new_branch.base)
1052
self.assertIs(None, specific_files)
1053
self.assertIs(None, extra_trees)
1055
def test_with_rev_specs(self):
1056
tree = self.make_branch_and_tree('tree')
1057
self.build_tree_contents([('tree/file', b'oldcontent')])
1058
tree.add('file', b'file-id')
1059
tree.commit('old tree', timestamp=0, rev_id=b"old-id")
1060
self.build_tree_contents([('tree/file', b'newcontent')])
1061
tree.commit('new tree', timestamp=0, rev_id=b"new-id")
1063
revisions = [revisionspec.RevisionSpec.from_string('1'),
1064
revisionspec.RevisionSpec.from_string('2')]
1065
(old_tree, new_tree,
1066
old_branch, new_branch,
1067
specific_files, extra_trees) = self.call_gtabtd(
1068
['tree'], revisions, None, None)
1070
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1071
self.assertEqual(b"old-id", old_tree.get_revision_id())
1072
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1073
self.assertEqual(b"new-id", new_tree.get_revision_id())
1074
self.assertEqual(tree.branch.base, old_branch.base)
1075
self.assertEqual(tree.branch.base, new_branch.base)
1076
self.assertIs(None, specific_files)
1077
self.assertEqual(tree.basedir, extra_trees[0].basedir)
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)