2
# -*- coding: UTF-8 -*-
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
from trace import mutter
21
from errors import BzrError
24
def diff_trees(old_tree, new_tree):
25
"""Compute diff between two trees.
27
They may be in different branches and may be working or historical
30
This only compares the versioned files, paying no attention to
31
files which are ignored or unknown. Those can only be present in
32
working trees and can be reported on separately.
34
Yields a sequence of (state, id, old_name, new_name, kind).
35
Each filename and each id is listed only once.
37
## TODO: Allow specifying a list of files to compare, rather than
38
## doing the whole tree? (Not urgent.)
40
## TODO: Allow diffing any two inventories, not just the
41
## current one against one. We mgiht need to specify two
42
## stores to look for the files if diffing two branches. That
43
## might imply this shouldn't be primarily a Branch method.
45
sha_match_cnt = modified_cnt = 0
47
old_it = old_tree.list_files()
48
new_it = new_tree.list_files()
56
old_item = next(old_it)
57
new_item = next(new_it)
59
# We step through the two sorted iterators in parallel, trying to
62
while (old_item != None) or (new_item != None):
63
# OK, we still have some remaining on both, but they may be
66
old_name, old_class, old_kind, old_id = old_item
71
new_name, new_class, new_kind, new_id = new_item
76
# can't handle the old tree being a WorkingTree
77
assert old_class == 'V'
79
if new_item and (new_class != 'V'):
80
yield new_class, None, None, new_name, new_kind
81
new_item = next(new_it)
82
elif (not new_item) or (old_item and (old_name < new_name)):
83
if new_tree.has_id(old_id):
84
# will be mentioned as renamed under new name
87
yield 'D', old_id, old_name, None, old_kind
88
old_item = next(old_it)
89
elif (not old_item) or (new_item and (new_name < old_name)):
90
if old_tree.has_id(new_id):
91
yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
93
yield 'A', new_id, None, new_name, new_kind
94
new_item = next(new_it)
95
elif old_id != new_id:
96
assert old_name == new_name
97
# both trees have a file of this name, but it is not the
98
# same file. in other words, the old filename has been
99
# overwritten by either a newly-added or a renamed file.
100
# (should we return something about the overwritten file?)
101
if old_tree.has_id(new_id):
102
# renaming, overlying a deleted file
103
yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
105
yield 'A', new_id, None, new_name, new_kind
107
new_item = next(new_it)
108
old_item = next(old_it)
110
assert old_id == new_id
111
assert old_id != None
112
assert old_name == new_name
113
assert old_kind == new_kind
115
if old_kind == 'directory':
116
yield '.', new_id, old_name, new_name, new_kind
117
elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id):
119
yield '.', new_id, old_name, new_name, new_kind
122
yield 'M', new_id, old_name, new_name, new_kind
124
new_item = next(new_it)
125
old_item = next(old_it)
128
mutter("diff finished: %d SHA matches, %d modified"
129
% (sha_match_cnt, modified_cnt))
133
def _diff_one(oldlines, newlines, to_file, **kw):
136
# FIXME: difflib is wrong if there is no trailing newline.
137
# The syntax used by patch seems to be "\ No newline at
138
# end of file" following the last diff line from that
139
# file. This is not trivial to insert into the
140
# unified_diff output and it might be better to just fix
141
# or replace that function.
143
# In the meantime we at least make sure the patch isn't
147
# Special workaround for Python2.3, where difflib fails if
148
# both sequences are empty.
149
if not oldlines and not newlines:
154
if oldlines and (oldlines[-1][-1] != '\n'):
157
if newlines and (newlines[-1][-1] != '\n'):
161
ud = difflib.unified_diff(oldlines, newlines, **kw)
163
# work-around for difflib being too smart for its own good
164
# if /dev/null is "1,0", patch won't recognize it as /dev/null
167
ud[2] = ud[2].replace('-1,0', '-0,0')
170
ud[2] = ud[2].replace('+1,0', '+0,0')
172
to_file.writelines(ud)
174
print >>to_file, "\\ No newline at end of file"
178
def show_diff(b, revision, file_list):
182
raise NotImplementedError('diff on restricted files broken at the moment')
185
old_tree = b.basis_tree()
187
old_tree = b.revision_tree(b.lookup_revision(revision))
189
new_tree = b.working_tree()
191
# TODO: Options to control putting on a prefix or suffix, perhaps as a format string
195
DEVNULL = '/dev/null'
196
# Windows users, don't panic about this filename -- it is a
197
# special signal to GNU patch that the file should be created or
198
# deleted respectively.
200
# TODO: Generation of pseudo-diffs for added/deleted files could
201
# be usefully made into a much faster special case.
203
delta = compare_trees(old_tree, new_tree, want_unchanged=False)
205
for path, file_id, kind in delta.removed:
206
print '*** removed %s %r' % (kind, path)
208
_diff_one(old_tree.get_file(file_id).readlines(),
211
fromfile=old_label + path,
214
for path, file_id, kind in delta.added:
215
print '*** added %s %r' % (kind, path)
218
new_tree.get_file(file_id).readlines(),
221
tofile=new_label + path)
223
for old_path, new_path, file_id, kind, text_modified in delta.renamed:
224
print '*** renamed %s %r => %r' % (kind, old_path, new_path)
226
_diff_one(old_tree.get_file(file_id).readlines(),
227
new_tree.get_file(file_id).readlines(),
229
fromfile=old_label + old_path,
230
tofile=new_label + new_path)
232
for path, file_id, kind in delta.modified:
233
print '*** modified %s %r' % (kind, path)
235
_diff_one(old_tree.get_file(file_id).readlines(),
236
new_tree.get_file(file_id).readlines(),
238
fromfile=old_label + path,
239
tofile=new_label + path)
244
"""Describes changes from one tree to another.
253
(oldpath, newpath, id, kind, text_modified)
259
Each id is listed only once.
261
Files that are both modified and renamed are listed only in
262
renamed, with the text_modified flag true.
264
The lists are normally sorted when the delta is created.
273
def show(self, to_file, show_ids=False, show_unchanged=False):
274
def show_list(files):
275
for path, fid, kind in files:
276
if kind == 'directory':
278
elif kind == 'symlink':
282
print >>to_file, ' %-30s %s' % (path, fid)
284
print >>to_file, ' ', path
287
print >>to_file, 'removed:'
288
show_list(self.removed)
291
print >>to_file, 'added:'
292
show_list(self.added)
295
print >>to_file, 'renamed:'
296
for oldpath, newpath, fid, kind, text_modified in self.renamed:
298
print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid)
300
print >>to_file, ' %s => %s' % (oldpath, newpath)
303
print >>to_file, 'modified:'
304
show_list(self.modified)
306
if show_unchanged and self.unchanged:
307
print >>to_file, 'unchanged:'
308
show_list(self.unchanged)
312
def compare_trees(old_tree, new_tree, want_unchanged):
313
old_inv = old_tree.inventory
314
new_inv = new_tree.inventory
316
mutter('start compare_trees')
317
for file_id in old_tree:
318
if file_id in new_tree:
319
old_path = old_inv.id2path(file_id)
320
new_path = new_inv.id2path(file_id)
322
kind = old_inv.get_file_kind(file_id)
323
assert kind == new_inv.get_file_kind(file_id)
325
assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
326
'invalid file kind %r' % kind
328
old_sha1 = old_tree.get_file_sha1(file_id)
329
new_sha1 = new_tree.get_file_sha1(file_id)
330
text_modified = (old_sha1 != new_sha1)
332
## mutter("no text to check for %r %r" % (file_id, kind))
333
text_modified = False
335
# TODO: Can possibly avoid calculating path strings if the
336
# two files are unchanged and their names and parents are
337
# the same and the parents are unchanged all the way up.
338
# May not be worthwhile.
340
if old_path != new_path:
341
delta.renamed.append((old_path, new_path, file_id, kind,
344
delta.modified.append((new_path, file_id, kind))
346
delta.unchanged.append((new_path, file_id, kind))
348
delta.removed.append((old_inv.id2path(file_id), file_id, kind))
350
mutter('start looking for new files')
351
for file_id in new_inv:
352
if file_id in old_inv:
354
kind = new_inv.get_file_kind(file_id)
355
delta.added.append((new_inv.id2path(file_id), file_id, kind))
360
delta.modified.sort()
361
delta.unchanged.sort()