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
Yields a sequence of (state, id, old_name, new_name, kind).
31
Each filename and each id is listed only once.
34
## TODO: Compare files before diffing; only mention those that have changed
36
## TODO: Set nice names in the headers, maybe include diffstat
38
## TODO: Perhaps make this a generator rather than using
41
## TODO: Allow specifying a list of files to compare, rather than
42
## doing the whole tree? (Not urgent.)
44
## TODO: Allow diffing any two inventories, not just the
45
## current one against one. We mgiht need to specify two
46
## stores to look for the files if diffing two branches. That
47
## might imply this shouldn't be primarily a Branch method.
49
## XXX: This doesn't report on unknown files; that can be done
50
## from a separate method.
52
sha_match_cnt = modified_cnt = 0
54
old_it = old_tree.list_files()
55
new_it = new_tree.list_files()
63
old_item = next(old_it)
64
new_item = next(new_it)
66
# We step through the two sorted iterators in parallel, trying to
69
while (old_item != None) or (new_item != None):
70
# OK, we still have some remaining on both, but they may be
73
old_name, old_class, old_kind, old_id = old_item
78
new_name, new_class, new_kind, new_id = new_item
83
# can't handle the old tree being a WorkingTree
84
assert old_class == 'V'
86
if new_item and (new_class != 'V'):
87
yield new_class, None, None, new_name, new_kind
88
new_item = next(new_it)
89
elif (not new_item) or (old_item and (old_name < new_name)):
90
if new_tree.has_id(old_id):
91
# will be mentioned as renamed under new name
94
yield 'D', old_id, old_name, None, old_kind
95
old_item = next(old_it)
96
elif (not old_item) or (new_item and (new_name < old_name)):
97
if old_tree.has_id(new_id):
98
yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
100
yield 'A', new_id, None, new_name, new_kind
101
new_item = next(new_it)
102
elif old_id != new_id:
103
assert old_name == new_name
104
# both trees have a file of this name, but it is not the
105
# same file. in other words, the old filename has been
106
# overwritten by either a newly-added or a renamed file.
107
# (should we return something about the overwritten file?)
108
if old_tree.has_id(new_id):
109
# renaming, overlying a deleted file
110
yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
112
yield 'A', new_id, None, new_name, new_kind
114
new_item = next(new_it)
115
old_item = next(old_it)
117
assert old_id == new_id
118
assert old_id != None
119
assert old_name == new_name
120
assert old_kind == new_kind
122
if old_kind == 'directory':
123
yield '.', new_id, old_name, new_name, new_kind
124
elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id):
126
yield '.', new_id, old_name, new_name, new_kind
129
yield 'M', new_id, old_name, new_name, new_kind
131
new_item = next(new_it)
132
old_item = next(old_it)
135
mutter("diff finished: %d SHA matches, %d modified"
136
% (sha_match_cnt, modified_cnt))
140
def show_diff(b, revision, file_list):
141
import difflib, sys, types
144
old_tree = b.basis_tree()
146
old_tree = b.revision_tree(b.lookup_revision(revision))
148
new_tree = b.working_tree()
150
# TODO: Options to control putting on a prefix or suffix, perhaps as a format string
154
DEVNULL = '/dev/null'
155
# Windows users, don't panic about this filename -- it is a
156
# special signal to GNU patch that the file should be created or
157
# deleted respectively.
159
# TODO: Generation of pseudo-diffs for added/deleted files could
160
# be usefully made into a much faster special case.
162
# TODO: Better to return them in sorted order I think.
165
file_list = [b.relpath(f) for f in file_list]
167
# FIXME: If given a file list, compare only those files rather
168
# than comparing everything and then throwing stuff away.
170
for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree):
172
if file_list and (new_name not in file_list):
175
# Don't show this by default; maybe do it if an option is passed
176
# idlabel = ' {%s}' % fid
179
def diffit(oldlines, newlines, **kw):
181
# FIXME: difflib is wrong if there is no trailing newline.
182
# The syntax used by patch seems to be "\ No newline at
183
# end of file" following the last diff line from that
184
# file. This is not trivial to insert into the
185
# unified_diff output and it might be better to just fix
186
# or replace that function.
188
# In the meantime we at least make sure the patch isn't
192
# Special workaround for Python2.3, where difflib fails if
193
# both sequences are empty.
194
if not oldlines and not newlines:
199
if oldlines and (oldlines[-1][-1] != '\n'):
202
if newlines and (newlines[-1][-1] != '\n'):
206
ud = difflib.unified_diff(oldlines, newlines, **kw)
208
# work-around for difflib being too smart for its own good
209
# if /dev/null is "1,0", patch won't recognize it as /dev/null
212
ud[2] = ud[2].replace('-1,0', '-0,0')
215
ud[2] = ud[2].replace('+1,0', '+0,0')
217
sys.stdout.writelines(ud)
219
print "\\ No newline at end of file"
220
sys.stdout.write('\n')
222
if file_state in ['.', '?', 'I']:
224
elif file_state == 'A':
225
print '*** added %s %r' % (kind, new_name)
228
new_tree.get_file(fid).readlines(),
230
tofile=new_label + new_name + idlabel)
231
elif file_state == 'D':
232
assert isinstance(old_name, types.StringTypes)
233
print '*** deleted %s %r' % (kind, old_name)
235
diffit(old_tree.get_file(fid).readlines(), [],
236
fromfile=old_label + old_name + idlabel,
238
elif file_state in ['M', 'R']:
239
if file_state == 'M':
240
assert kind == 'file'
241
assert old_name == new_name
242
print '*** modified %s %r' % (kind, new_name)
243
elif file_state == 'R':
244
print '*** renamed %s %r => %r' % (kind, old_name, new_name)
247
diffit(old_tree.get_file(fid).readlines(),
248
new_tree.get_file(fid).readlines(),
249
fromfile=old_label + old_name + idlabel,
250
tofile=new_label + new_name)
252
raise BzrError("can't represent state %s {%s}" % (file_state, fid))
257
"""Describes changes from one tree to another.
266
(oldpath, newpath, id)
270
A path may occur in more than one list if it was e.g. deleted
271
under an old id and renamed into place in a new id.
273
Files are listed in either modified or renamed, not both. In
274
other words, renamed files may also be modified.
283
def compare_inventories(old_inv, new_inv):
284
"""Return a TreeDelta object describing changes between inventories.
286
This only describes changes in the shape of the tree, not the
289
This is an alternative to diff_trees() and should probably
290
eventually replace it.
292
old_ids = old_inv.id_set()
293
new_ids = new_inv.id_set()
296
delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)]
299
delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)]
302
for fid in old_ids & new_ids:
303
old_ie = old_inv[fid]
304
new_ie = new_inv[fid]
305
old_path = old_inv.id2path(fid)
306
new_path = new_inv.id2path(fid)
308
if old_path != new_path:
309
delta.renamed.append((old_path, new_path, fid))
310
elif old_ie.text_sha1 != new_ie.text_sha1:
311
delta.modified.append((new_path, fid))
313
delta.modified.sort()