Skip to content

Commit 2a5e600

Browse files
authored
Merge pull request #42 from nexB/4-handle-moved-files
4 handle moved files
2 parents 4043e53 + 8accf3c commit 2a5e600

39 files changed

Lines changed: 12426 additions & 74 deletions

src/deltacode/__init__.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ def __init__(self, new_path, old_path):
4646
self.deltas = OrderedDict([
4747
('added', []),
4848
('removed', []),
49+
('moved', []),
4950
('modified', []),
5051
('unmodified', [])
5152
])
5253

5354
if self.new.path != '' and self.old.path != '':
5455
self.determine_delta()
56+
self.determine_moved()
5557

5658
def align_scan(self):
5759
"""
@@ -128,30 +130,86 @@ def determine_delta(self):
128130
assert new_files_visited == self.new.files_count, "Number of visited files({})) does not match total_files({}) in the new scan".format(new_files_visited, self.new.files_count)
129131
assert old_files_visited == self.old.files_count, "Number of visited files({})) does not match total_files({}) in the old scan".format(old_files_visited, self.old.files_count)
130132

133+
def determine_moved(self):
134+
"""
135+
Modify the OrderedDict of Delta objects by creating an index of
136+
'removed' Delta objects and an index of 'added' Delta objects indexed
137+
by their 'sha1' attribute, identifying any unique pairs of Deltas in
138+
both indices with the same 'sha1' and File 'name' attributes, and
139+
converting each such pair of 'added' and 'removed' Delta objects to a
140+
'moved' Delta object.
141+
"""
142+
added = self.index_deltas('sha1', [i for i in self.deltas['added']])
143+
removed = self.index_deltas('sha1', [i for i in self.deltas['removed']])
144+
145+
# TODO: should it be iteritems() or items()
146+
for added_sha1, added_deltas in added.iteritems():
147+
for removed_sha1, removed_deltas in removed.iteritems():
148+
149+
# check for matching sha1s on both sides
150+
if utils.check_moved(added_sha1, added_deltas, removed_sha1, removed_deltas):
151+
self.update_deltas(added_deltas.pop(), removed_deltas.pop())
152+
153+
def update_deltas(self, added, removed):
154+
"""
155+
Convert the matched 'added' and 'removed' Delta objects to a combined
156+
'moved' Delta object and delete the 'added' and 'removed' objects.
157+
"""
158+
self.deltas.get('moved').append(Delta(added.new_file, removed.old_file, 'moved'))
159+
self.deltas.get('added').remove(added)
160+
self.deltas.get('removed').remove(removed)
161+
162+
def index_deltas(self, index_key='path', delta_list=[]):
163+
"""
164+
Return a dictionary of a list of Delta objects indexed by the key
165+
passed via the 'index_key' variable. If no 'index_key' variable is
166+
passed, the dict is keyed by the Delta object's 'path' variable. For a
167+
'removed' Delta object, use the variable from the 'old_file'; for all
168+
other Delta objects (e.g., 'added'), use the 'new_file'. This function
169+
does not currently catch the AttributeError exception.
170+
"""
171+
index = {}
172+
173+
for delta in delta_list:
174+
if delta.category == 'removed':
175+
key = getattr(delta.old_file, index_key)
176+
else:
177+
key = getattr(delta.new_file, index_key)
178+
179+
if index.get(key) is None:
180+
index[key] = []
181+
index[key].append(delta)
182+
else:
183+
index[key].append(delta)
184+
185+
return index
186+
131187
def get_stats(self):
132188
"""
133189
Given a list of Delta objects, return a 'counts' dictionary keyed by
134190
category -- i.e., the keys of the determine_delta() OrderedDict of
135191
Delta objects -- that contains the count as a value for each category.
136192
"""
137-
added, modified, removed, unmodified = 0, 0, 0, 0
193+
added, modified, moved, removed, unmodified = 0, 0, 0, 0, 0
138194

139195
added = len(self.deltas['added'])
140196
modified = len(self.deltas['modified'])
197+
moved = len(self.deltas['moved'])
141198
removed = len(self.deltas['removed'])
142199
unmodified = len(self.deltas['unmodified'])
143200

144-
return OrderedDict([('added', added), ('modified', modified), ('removed', removed), ('unmodified', unmodified)])
201+
return OrderedDict([('added', added), ('modified', modified), ('moved', moved), ('removed', removed), ('unmodified', unmodified)])
145202

146203
def to_dict(self):
147204
"""
148205
Given an OrderedDict of Delta objects, return an OrderedDict of Delta
149-
objects grouping the objects under the keys 'added', 'removed',
206+
objects grouping the objects under the keys 'added', 'removed', 'moved',
150207
'modified' or 'unmodified'.
151208
"""
152209
return OrderedDict([
153210
('added', [d.to_dict() for d in self.deltas.get('added')]),
154211
('removed', [d.to_dict() for d in self.deltas.get('removed')]),
212+
('moved', [d.to_dict() for d in self.deltas.get('moved')]),
155213
('modified', [d.to_dict() for d in self.deltas.get('modified')]),
156214
('unmodified', [d.to_dict() for d in self.deltas.get('unmodified')]),
157215
])
@@ -161,7 +219,7 @@ class Delta(object):
161219
"""
162220
A tuple reflecting a comparison of two files -- each of which is a File
163221
object -- and the category that characterizes the comparison:
164-
'added', 'modified', 'removed' or 'unmodified'.
222+
'added', 'modified', 'moved', 'removed' or 'unmodified'.
165223
"""
166224
def __init__(self, new_file=None, old_file=None, delta_type=None):
167225
self.new_file = new_file if new_file else File()
@@ -219,6 +277,15 @@ def to_dict(self):
219277
('type', self.old_file.type),
220278
('size', self.old_file.size)
221279
])
280+
elif self.category == 'moved':
281+
return OrderedDict([
282+
('category', 'moved'),
283+
('path', self.new_file.path),
284+
('old_path', self.old_file.path),
285+
('name', self.new_file.name),
286+
('type', self.new_file.type),
287+
('size', self.new_file.size)
288+
])
222289
elif self.category == 'modified':
223290
return OrderedDict([
224291
('category', 'modified'),

src/deltacode/cli.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,14 @@ def write_csv(delta, result_file):
4646
"""
4747
with open(result_file, 'wb') as out:
4848
csv_out = csv.writer(out)
49-
csv_out.writerow(['Type of delta', 'Path', 'Name', 'Type', 'Size'])
49+
csv_out.writerow(['Type of delta', 'Path', 'Name', 'Type', 'Size', 'Old Path'])
5050
for row in [(
5151
f.category,
5252
f.old_file.path if f.category == 'removed' else f.new_file.path,
5353
f.old_file.name if f.category == 'removed' else f.new_file.name,
5454
f.old_file.type if f.category == 'removed' else f.new_file.type,
55-
f.old_file.size if f.category == 'removed' else f.new_file.size)
55+
f.old_file.size if f.category == 'removed' else f.new_file.size,
56+
f.old_file.path if f.category == 'moved' else '')
5657
for d in delta.deltas for f in delta.deltas.get(d)]:
5758
csv_out.writerow(row)
5859

src/deltacode/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,16 @@ def fix_trees(a_files, b_files):
101101
for b_file in b_files:
102102
b_file.original_path = b_file.path
103103
b_file.path = '/'.join(paths.split(b_file.path)[b_offset:])
104+
105+
106+
def check_moved(added_sha1, added_deltas, removed_sha1, removed_deltas):
107+
"""
108+
Return True if there is only one pair of matching 'added' and 'removed'
109+
Delta objects and their respective File objects have the same 'name' attribute.
110+
"""
111+
if added_sha1 != removed_sha1:
112+
return False
113+
if len(added_deltas) != 1 or len(removed_deltas) != 1:
114+
return False
115+
if added_deltas[0].new_file.name == removed_deltas[0].old_file.name:
116+
return True

tests/data/cli/1_file_moved.csv

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Type of delta,Path,Name,Type,Size,Old Path
2+
moved,b/a4.py,a4.py,file,200,a/a4.py
3+
unmodified,a/a3.py,a3.py,file,200,
4+
unmodified,b/b4.py,b4.py,file,200,
5+
unmodified,a/a2.py,a2.py,file,200,
6+
unmodified,b/b2.py,b2.py,file,200,
7+
unmodified,b/b1.py,b1.py,file,200,
8+
unmodified,b/b3.py,b3.py,file,200,
9+
unmodified,a/a1.py,a1.py,file,200,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Type of delta,Path,Name,Type,Size,Old Path
2+
added,b/a4.py,a4.py,file,200,
3+
added,b/a4_copy.py,a4_copy.py,file,200,
4+
removed,a/a4.py,a4.py,file,200,
5+
unmodified,a/a3.py,a3.py,file,200,
6+
unmodified,b/b4.py,b4.py,file,200,
7+
unmodified,a/a2.py,a2.py,file,200,
8+
unmodified,b/b2.py,b2.py,file,200,
9+
unmodified,b/b1.py,b1.py,file,200,
10+
unmodified,b/b3.py,b3.py,file,200,
11+
unmodified,a/a1.py,a1.py,file,200,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Type of delta,Path,Name,Type,Size,Old Path
2+
added,b/a4.py,a4.py,file,200,
3+
added,c/a4.py,a4.py,file,200,
4+
removed,a/a4.py,a4.py,file,200,
5+
unmodified,a/a3.py,a3.py,file,200,
6+
unmodified,b/b4.py,b4.py,file,200,
7+
unmodified,a/a2.py,a2.py,file,200,
8+
unmodified,b/b2.py,b2.py,file,200,
9+
unmodified,b/b1.py,b1.py,file,200,
10+
unmodified,b/b3.py,b3.py,file,200,
11+
unmodified,a/a1.py,a1.py,file,200,

tests/data/cli/added1.csv

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
Type of delta,Path,Name,Type,Size
2-
added,a/a5.py,a5.py,file,200
3-
unmodified,a/a3.py,a3.py,file,200
4-
unmodified,b/b4.py,b4.py,file,200
5-
unmodified,a/a2.py,a2.py,file,200
6-
unmodified,b/b2.py,b2.py,file,200
7-
unmodified,b/b1.py,b1.py,file,200
8-
unmodified,b/b3.py,b3.py,file,200
9-
unmodified,a/a4.py,a4.py,file,200
10-
unmodified,a/a1.py,a1.py,file,200
1+
Type of delta,Path,Name,Type,Size,Old Path
2+
added,a/a5.py,a5.py,file,200,
3+
unmodified,a/a3.py,a3.py,file,200,
4+
unmodified,b/b4.py,b4.py,file,200,
5+
unmodified,a/a2.py,a2.py,file,200,
6+
unmodified,b/b2.py,b2.py,file,200,
7+
unmodified,b/b1.py,b1.py,file,200,
8+
unmodified,b/b3.py,b3.py,file,200,
9+
unmodified,a/a4.py,a4.py,file,200,
10+
unmodified,a/a1.py,a1.py,file,200,
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Type of delta,Path,Name,Type,Size
2-
license info added,some/path/a/a1.py,a1.py,file,350
3-
unmodified,some/path/b/b1.py,b1.py,file,290
1+
Type of delta,Path,Name,Type,Size,Old Path
2+
license info added,some/path/a/a1.py,a1.py,file,350,
3+
unmodified,some/path/b/b1.py,b1.py,file,290,
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Type of delta,Path,Name,Type,Size
2-
license info added,some/path/a/a1.py,a1.py,file,350
3-
unmodified,some/path/b/b1.py,b1.py,file,290
1+
Type of delta,Path,Name,Type,Size,Old Path
2+
license info added,some/path/a/a1.py,a1.py,file,350,
3+
unmodified,some/path/b/b1.py,b1.py,file,290,
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Type of delta,Path,Name,Type,Size
2-
license info removed,some/path/a/a1.py,a1.py,file,350
3-
unmodified,some/path/b/b1.py,b1.py,file,290
1+
Type of delta,Path,Name,Type,Size,Old Path
2+
license info removed,some/path/a/a1.py,a1.py,file,350,
3+
unmodified,some/path/b/b1.py,b1.py,file,290,

0 commit comments

Comments
 (0)