Skip to content

Commit 21a14fb

Browse files
committed
Lazily loading pull request diffs
1 parent a27e374 commit 21a14fb

File tree

5 files changed

+76
-59
lines changed

5 files changed

+76
-59
lines changed

webdiff/argparser.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,5 +96,4 @@ def diff_for_args(args):
9696

9797
if 'github' in args:
9898
gh = args['github']
99-
a_dir, b_dir = github_fetcher.fetch_pull_request(gh['owner'], gh['repo'], gh['num'])
100-
return dirdiff.diff(a_dir, b_dir)
99+
return github_fetcher.fetch_pull_request(gh['owner'], gh['repo'], gh['num'])

webdiff/diff.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def get_thick_dict(diff):
2424
d = get_thin_dict(diff)
2525
d.update({
2626
'is_image_diff': is_image_diff(diff),
27-
'no_changes': diff.no_changes()
27+
'no_changes': no_changes(diff)
2828
})
2929
if d['is_image_diff']:
3030
if d['a']: d['image_a'] = util.image_metadata(diff.a_path)
@@ -50,6 +50,12 @@ def get_thin_list(diffs, thick_idx=None):
5050
return ds
5151

5252

53+
def no_changes(diff):
54+
if diff.a_path and diff.b_path:
55+
return util.are_files_identical(diff.a_path, diff.b_path)
56+
return False
57+
58+
5359
def is_image_diff(diff):
5460
'''Determine whether this diff is appropriate for image diff UI.
5561

webdiff/github_fetcher.py

Lines changed: 68 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
This will create symlinks or clone git repos as needed.
44
"""
55

6+
# Use this PR for testing to see all four types of change at once:
7+
# https://github.com/danvk/test-repo/pull/2/
8+
69
import atexit
710
from collections import OrderedDict
811
import errno
@@ -14,19 +17,68 @@
1417

1518
from github import Github, UnknownObjectException
1619

20+
from util import memoize
21+
22+
23+
class GitHubDiff(object):
24+
def __init__(self, pr, github_file):
25+
self._pr = pr
26+
self._file = github_file
27+
self.type = {
28+
'modified': 'change',
29+
'renamed': 'move',
30+
'added': 'add',
31+
'removed': 'delete'
32+
}[github_file.status]
33+
self._a_path = None
34+
self._b_path = None
35+
36+
@property
37+
def a(self):
38+
if self.type == 'move':
39+
return self._file.raw_data['previous_filename']
40+
elif self.type == 'add':
41+
return None
42+
else:
43+
return self._file.filename
1744

18-
temp_dirs = []
45+
@property
46+
def b(self):
47+
if self.type == 'delete':
48+
return None
49+
else:
50+
return self._file.filename
1951

20-
_github_memo = None
21-
def _github():
22-
global _github_memo
23-
if _github_memo: return _github_memo
52+
# NB: these are @memoize'd via fetch()
53+
@property
54+
def a_path(self):
55+
return fetch(self._pr.base.repo, self.a, self._pr.base.sha)
56+
57+
@property
58+
def b_path(self):
59+
return fetch(self._pr.head.repo, self.b, self._pr.head.sha)
60+
61+
def __repr__(self):
62+
return '%s (%s)' % (self.a or self.b, self.type)
63+
64+
# TOOD: diffstats are accessible via file.{changes,additions,deletions}
65+
66+
67+
@memoize
68+
def fetch(repo, filename, sha):
69+
if filename is None: return None
70+
data = repo.get_file_contents(filename, sha).decoded_content
71+
_, ext = os.path.splitext(filename)
72+
fd, path = tempfile.mkstemp(suffix=ext)
73+
open(path, 'wb').write(data)
74+
return path
2475

76+
77+
@memoize
78+
def _github():
2579
def simple_fallback(message=None):
26-
global _github_memo
2780
if message: sys.stderr.write(message + '\n')
28-
_github_memo = Github()
29-
return _github_memo
81+
return Github()
3082

3183
github_rc = os.path.join(os.path.expanduser('~'), '.githubrc')
3284
if os.path.exists(github_rc):
@@ -45,53 +97,20 @@ def simple_fallback(message=None):
4597
return simple_fallback('.githubrc missing user.login. Using anonymous API access.')
4698
if not kvs.get('user.password'):
4799
return simple_fallback('.githubrc missing user.password. Using anonymous API access.')
48-
_github_memo = Github(kvs['user.login'], kvs['user.password'])
100+
return Github(kvs['user.login'], kvs['user.password'])
49101
else:
50-
_github_memo = Github()
51-
return _github_memo
52-
53-
54-
def put_in_dir(dirname, filename, contents):
55-
"""Puts contents into filename in dirname.
56-
57-
This creates intermediate directories as needed, e.g. if filename is a/b/c.
58-
"""
59-
d = os.path.join(dirname, os.path.dirname(filename))
60-
try:
61-
os.makedirs(d)
62-
except OSError, e:
63-
# be happy if someone already created the path
64-
if e.errno != errno.EEXIST:
65-
raise
66-
open(os.path.join(dirname, filename), 'w').write(contents)
102+
return Github()
67103

68104

69105
def fetch_pull_request(owner, repo, num):
70-
"""Pull down the pull request into two local directories.
71-
72-
Returns before_dir, after_dir.
73-
"""
74-
sys.stderr.write('Loading pull request %s/%s#%s from github...\n' % (owner, repo, num))
106+
'''Return a list of Diff objects for a pull request.'''
107+
sys.stderr.write('Loading pull request %s/%s#%s from github...\n' % (
108+
owner, repo, num))
75109
g = _github()
76110
pr = g.get_user(owner).get_repo(repo).get_pull(num)
77-
base_repo = pr.base.repo
78-
head_repo = pr.head.repo
79-
files = list(pr.get_files())
80-
81-
a_dir, b_dir = tempfile.mkdtemp(), tempfile.mkdtemp()
82-
temp_dirs.extend([a_dir, b_dir])
83-
84-
for f in files:
85-
sys.stderr.write(' %s...\n' % f.filename)
86-
if f.status == 'modified' or f.status == 'deleted':
87-
contents = base_repo.get_file_contents(f.filename, pr.base.sha).decoded_content
88-
put_in_dir(a_dir, f.filename, contents)
89-
if f.status == 'modified' or f.status == 'added':
90-
contents = head_repo.get_file_contents(f.filename, pr.head.sha).decoded_content
91-
put_in_dir(b_dir, f.filename, contents)
92-
# f.status == 'moved'?
93-
94-
return a_dir, b_dir
111+
files = pr.get_files()
112+
113+
return [GitHubDiff(pr, f) for f in files]
95114

96115

97116
class NoRemoteError(Exception):

webdiff/localfilediff.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,5 @@ def type(self):
4141
return 'move'
4242
return 'change'
4343

44-
def no_changes(self):
45-
if self.a_path and self.b_path:
46-
return util.are_files_identical(self.a_path, self.b_path)
47-
return False
48-
4944
def __repr__(self):
5045
return '%s/%s (%s)' % (self.a, self.b, self.type)

webdiff/util.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010

1111
from PIL import Image
1212

13-
import github_fetcher
14-
1513

1614
class ImageMagickNotAvailableError(Exception):
1715
pass

0 commit comments

Comments
 (0)