Skip to content

Commit 1094d41

Browse files
committed
Merge pull request #98 from danvk/lazy-diff
Make diff generation lazy
2 parents 371424a + eb02c16 commit 1094d41

File tree

13 files changed

+499
-342
lines changed

13 files changed

+499
-342
lines changed

tests/pair_test.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from nose.tools import *
44
from webdiff import util
5+
from webdiff import diff
6+
from webdiff import dirdiff
57

68
def test_pairing():
79
a_files = [
@@ -21,7 +23,7 @@ def test_pairing():
2123
'static/jsdifflib/diffview.js',
2224
'templates/heartbeat.html']
2325

24-
pairs = util.pair_files(a_files, b_files)
26+
pairs = dirdiff.pair_files(a_files, b_files)
2527
pairs.sort()
2628

2729
eq_(
@@ -34,31 +36,33 @@ def test_pairing():
3436
('templates/heartbeat.html', 'templates/heartbeat.html'),
3537
], pairs)
3638

39+
3740
def test_pairing_with_move():
3841
testdir = 'testdata/renamedfile'
39-
diff = util.find_diff('%s/left/dir' % testdir, '%s/right/dir' % testdir)
40-
eq_([{'a': 'file.json',
41-
'a_path': 'testdata/renamedfile/left/dir/file.json',
42-
'path': 'file.json',
42+
diffs = dirdiff.diff('%s/left/dir' % testdir, '%s/right/dir' % testdir)
43+
eq_([{
44+
'a': 'file.json',
4345
'b': 'renamed.json',
44-
'b_path': 'testdata/renamedfile/right/dir/renamed.json',
4546
'type': 'move',
46-
'no_changes': True,
47-
'idx': 0},
48-
{'a': 'file.json',
49-
'a_path': 'testdata/renamedfile/left/dir/file.json',
50-
'path': 'file.json',
47+
},
48+
{
49+
'a': 'file.json',
5150
'b': None,
52-
'b_path': None,
5351
'type': 'delete',
54-
'no_changes': False,
55-
'idx': 1}], diff)
52+
}], [diff.get_thin_dict(d) for d in diffs])
53+
54+
55+
class TinyDiff(object):
56+
def __init__(self, a, b):
57+
self.a_path = a
58+
self.b_path = b
59+
5660

5761
def test_is_image_diff():
58-
assert util.is_image_diff({'a': 'foo.png', 'b': 'bar.png'})
59-
assert not util.is_image_diff({'a': 'foo.png.gz', 'b': 'bar.png.gz'})
60-
assert not util.is_image_diff({'a': 'foo.txt', 'b': 'bar.txt'})
61-
assert util.is_image_diff({'a': 'foo.png', 'b': None})
62-
assert not util.is_image_diff({'a': 'foo.txt', 'b': None})
63-
assert util.is_image_diff({'a': None, 'b': 'foo.png'})
64-
assert not util.is_image_diff({'a': None, 'b': 'foo.txt'})
62+
assert diff.is_image_diff(TinyDiff('foo.png', 'bar.png'))
63+
assert not diff.is_image_diff(TinyDiff('foo.png.gz', 'bar.png.gz'))
64+
assert not diff.is_image_diff(TinyDiff('foo.txt', 'bar.txt'))
65+
assert diff.is_image_diff(TinyDiff('foo.png', None))
66+
assert not diff.is_image_diff(TinyDiff('foo.txt', None))
67+
assert diff.is_image_diff(TinyDiff(None, 'foo.png'))
68+
assert not diff.is_image_diff(TinyDiff(None, 'foo.txt'))

tests/util_test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
QUnit.test('util.filePairDisplayName', function(assert) {
2-
assert.deepEqual(filePairDisplayName({type: 'delete', path: 'dir/file.json'}),
2+
assert.deepEqual(filePairDisplayName({
3+
type: 'delete',
4+
a: 'dir/file.json',
5+
b: null
6+
}),
37
'dir/file.json');
48

59
var rename = function(a, b) {

webdiff/app.py

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from flask import (Flask, render_template, send_from_directory, send_file,
1818
request, jsonify, Response)
1919

20+
import diff
2021
import util
2122
import argparser
2223

@@ -46,8 +47,6 @@ class Config:
4647
app.config.from_object(Config)
4748
app.config.from_envvar('WEBDIFF_CONFIG', silent=True)
4849

49-
A_DIR = None
50-
B_DIR = None
5150
DIFF = None
5251
PORT = None
5352

@@ -75,20 +74,31 @@ def update_last_request_ms():
7574
LAST_REQUEST_MS = time.time() * 1000
7675

7776

77+
def error(code, message):
78+
e = {"code": code, "message": message}
79+
response = jsonify(e)
80+
response.status_code = 400
81+
return response
82+
83+
7884
@app.route("/<side>/get_contents", methods=['POST'])
7985
def get_contents(side):
80-
assert side in ('a', 'b')
86+
if side not in ('a', 'b'):
87+
return error('invalid side', 'Side must be "a" or "b", got %s' % side)
88+
89+
# TODO: switch to index? might be simpler
8190
path = request.form.get('path', '')
8291
if not path:
83-
e = {"code": "incomplete",
84-
"message": "Incomplete request (need path)"}
85-
response = jsonify(e)
86-
response.status_code = 400
87-
return response
92+
return error('incomplete', 'Incomplete request (need path)')
93+
94+
idx = diff.find_diff_index(DIFF, side, path)
95+
if idx is None:
96+
return error('not found', 'Invalid path on side %s: %s' % (side, path))
97+
98+
d = DIFF[idx]
99+
abs_path = d.a_path if side == 'a' else d.b_path
88100

89101
try:
90-
abs_path = os.path.join(A_DIR if side == 'a' else B_DIR,
91-
os.path.normpath(path)) # / --> \ on windows
92102
is_binary = util.is_binary_file(abs_path)
93103
if is_binary:
94104
size = os.path.getsize(abs_path)
@@ -97,51 +107,43 @@ def get_contents(side):
97107
contents = open(abs_path).read()
98108
return Response(contents, mimetype='text/plain')
99109
except Exception:
100-
e = {"code": "read-error",
101-
"message": "Unable to read %s" % abs_path}
102-
response = jsonify(e)
103-
response.status_code = 400
104-
return response
110+
return error('read-error', 'Unable to read %s' % abs_path)
105111

106112

107113
@app.route("/<side>/image/<path:path>")
108114
def get_image(side, path):
109-
assert side in ('a', 'b')
115+
if side not in ('a', 'b'):
116+
return error('invalid side', 'Side must be "a" or "b", got %s' % side)
117+
118+
# TODO: switch to index? might be simpler
110119
if not path:
111-
e = {"code": "incomplete",
112-
"message": "Incomplete request (need path)"}
113-
response = jsonify(e)
114-
response.status_code = 400
115-
return response
120+
return error('incomplete', 'Incomplete request (need path)')
116121

117122
mime_type, enc = mimetypes.guess_type(path)
118123
if not mime_type or not mime_type.startswith('image/') or enc is not None:
119-
e = {"code": "wrongtype",
120-
"message": "Requested file of type (%s, %s) as image" % (
121-
mime_type, enc)}
122-
response = jsonify(e)
123-
response.status_code = 400
124-
return response
124+
return error('wrongtype', 'Requested file of type (%s, %s) as image' % (
125+
mime_type, enc))
126+
127+
idx = diff.find_diff_index(DIFF, side, path)
128+
if idx is None:
129+
return error('not found', 'Invalid path on side %s: %s' % (side, path))
130+
131+
d = DIFF[idx]
132+
abs_path = d.a_path if side == 'a' else d.b_path
125133

126134
try:
127-
abs_path = os.path.join(A_DIR if side == 'a' else B_DIR,
128-
os.path.normpath(path)) # / --> \ on windows
129135
contents = open(abs_path).read()
130136
return Response(contents, mimetype=mime_type)
131137
except Exception:
132-
e = {"code": "read-error",
133-
"message": "Unable to read %s" % abs_path}
134-
response = jsonify(e)
135-
response.status_code = 400
136-
return response
138+
return error('read-error', 'Unable to read %s' % abs_path)
137139

138140

139141
@app.route("/pdiff/<int:idx>")
140142
def get_pdiff(idx):
141143
idx = int(idx)
142-
pair = DIFF[idx]
144+
d = DIFF[idx]
143145
try:
144-
_, pdiff_image = util.generate_pdiff_image(pair['a_path'], pair['b_path'])
146+
_, pdiff_image = util.generate_pdiff_image(d.a_path, d.b_path)
145147
dilated_image = util.generate_dilated_pdiff_image(pdiff_image)
146148
except util.ImageMagickNotAvailableError:
147149
return 'ImageMagick is not available', 501
@@ -153,9 +155,9 @@ def get_pdiff(idx):
153155
@app.route("/pdiffbbox/<int:idx>")
154156
def get_pdiff_bbox(idx):
155157
idx = int(idx)
156-
pair = DIFF[idx]
158+
d = DIFF[idx]
157159
try:
158-
_, pdiff_image = util.generate_pdiff_image(pair['a_path'], pair['b_path'])
160+
_, pdiff_image = util.generate_pdiff_image(d.a_path, d.b_path)
159161
bbox = util.get_pdiff_bbox(pdiff_image)
160162
except util.ImageMagickNotAvailableError:
161163
return 'ImageMagick is not available', 501
@@ -173,10 +175,17 @@ def index():
173175
@app.route("/<int:idx>")
174176
def file_diff(idx):
175177
idx = int(idx)
178+
pairs = diff.get_thin_list(DIFF)
176179
return render_template('file_diff.html',
177180
idx=idx,
178181
has_magick=util.is_imagemagick_available(),
179-
pairs=DIFF)
182+
pairs=pairs)
183+
184+
185+
@app.route('/thick/<int:idx>')
186+
def thick_diff(idx):
187+
idx = int(idx)
188+
return jsonify(diff.get_thick_dict(DIFF[idx]))
180189

181190

182191
@app.route('/favicon.ico')
@@ -254,24 +263,23 @@ def is_webdiff_from_head():
254263

255264

256265
def run():
257-
global A_DIR, B_DIR, DIFF, PORT
266+
global DIFF, PORT
258267
try:
259268
parsed_args = argparser.parse(sys.argv[1:])
260269
except argparser.UsageError as e:
261270
sys.stderr.write('Error: %s\n\n' % e.message)
262271
usage_and_die()
263272

264-
A_DIR, B_DIR, DIFF = util.diff_for_args(parsed_args)
273+
DIFF = argparser.diff_for_args(parsed_args)
265274

266275
if app.config['TESTING'] or app.config['DEBUG']:
267-
sys.stderr.write('Diffing:\nA: %s\nB: %s\n\n' % (A_DIR, B_DIR))
276+
sys.stderr.write('Diff:\n%s' % DIFF)
268277

269278
PORT = pick_a_port(parsed_args)
270279

271280
sys.stderr.write('''Serving diffs on http://localhost:%s
272281
Close the browser tab or hit Ctrl-C when you're done.
273282
''' % PORT)
274-
logging.info('Diff: %r', DIFF)
275283
Timer(0.1, open_browser).start()
276284
app.run(host='0.0.0.0', port=PORT)
277285

webdiff/argparser.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import os
44
import re
55

6+
import dirdiff
7+
import githubdiff
68
import github_fetcher
9+
from localfilediff import LocalFileDiff
710

811
class UsageError(Exception):
912
pass
@@ -72,3 +75,26 @@ def parse(args):
7275
out['files'] = (a, b)
7376

7477
return out
78+
79+
80+
# TODO: move into dirdiff?
81+
def _shim_for_file_diff(a_file, b_file):
82+
'''Sets A_DIR, B_DIR and DIFF to do a one-file diff.'''
83+
dirname = os.path.dirname
84+
basename = os.path.basename
85+
return LocalFileDiff(dirname(a_file), basename(a_file),
86+
dirname(b_file), basename(b_file),
87+
False) # probably not a move
88+
89+
90+
def diff_for_args(args):
91+
'''Returns a list of Diff objects for parsed command line args.'''
92+
if 'dirs' in args:
93+
return dirdiff.diff(*args['dirs'])
94+
95+
if 'files' in args:
96+
return [_shim_for_file_diff(*args['files'])]
97+
98+
if 'github' in args:
99+
gh = args['github']
100+
return githubdiff.fetch_pull_request(gh['owner'], gh['repo'], gh['num'])

0 commit comments

Comments
 (0)