Klaus Demo ~jonashaag/klaus / e21cf6a
Refactor to prepare for caching overhaul. Refs #248 Jonas Haag 6 months ago
3 changed file(s) with 65 addition(s) and 101 deletion(s). Raw diff Collapse all Expand all
1616 NOT_SET = '__not_set__'
1717
1818
19 def cached_call(key, validator, producer, _cache={}):
20 data, old_validator = _cache.get(key, (None, NOT_SET))
21 if old_validator != validator:
22 data = producer()
23 _cache[key] = (data, validator)
24 return data
25
26
2719 class FancyRepo(dulwich.repo.Repo):
2820 """A wrapper around Dulwich's Repo that adds some helper methods."""
2921 @property
3022 def name(self):
3123 return repo_human_name(self.path)
3224
25 def getref(self, k, default=NOT_SET):
26 try:
27 return self[k]
28 except KeyError:
29 if default is not NOT_SET:
30 return default
31 else:
32 raise
33
34 def get_refs_as_dict(self, base=None):
35 return self.refs.as_dict(base)
36
37 def get_resolved_refs_as_dict(self, base=None, resolve_default=NOT_SET):
38 res = {}
39 for k, v in self.get_refs_as_dict(base).items():
40 v = self.getref(v, default=None)
41 if v is None and resolve_default is NOT_SET:
42 # Skip unresolvable refs when no default is given.
43 pass
44 else:
45 res[k] = v or resolve_default
46 return res
47
3348 # TODO: factor out stuff into dulwich
3449 def get_last_updated_at(self):
3550 """Get datetime of last commit to this repository."""
36 # Cache result to speed up repo_list.html template.
37 # If self.get_refs() has changed, we should invalidate the cache.
38 all_refs = self.get_refs()
39 return cached_call(
40 key=(id(self), 'get_last_updated_at'),
41 validator=all_refs,
42 producer=lambda: self._get_last_updated_at(all_refs)
43 )
44
45 def _get_last_updated_at(self, all_refs):
46 resolveable_refs = []
47 for ref_hash in all_refs:
48 try:
49 resolveable_refs.append(self[ref_hash])
50 except KeyError:
51 # Whoops. The ref points at a non-existant object
52 pass
53 resolveable_refs.sort(
54 key=lambda obj:getattr(obj, 'commit_time', float('-inf')),
55 reverse=True
56 )
57 for ref in resolveable_refs:
58 # Find the latest ref that has a commit_time; tags do not
59 # have a commit time
60 if hasattr(ref, "commit_time"):
61 return ref.commit_time
62 return None
51 commit_times = [getattr(obj, 'commit_time', float('-inf'))
52 for obj in self.get_resolved_refs_as_dict().values()]
53 if commit_times:
54 return max(commit_times)
55 else:
56 return None
6357
6458 @property
6559 def cloneurl(self):
7771 """Like Dulwich's `get_description`, but returns None if the file
7872 contains Git's default text "Unnamed repository[...]".
7973 """
80 # Cache result to speed up repo_list.html template.
81 # If description file mtime has changed, we should invalidate the cache.
82 description_file = os.path.join(self._controldir, 'description')
83 try:
84 description_mtime = os.stat(os.path.join(self._controldir, 'description')).st_mtime
85 except OSError:
86 description_mtime = None
87
88 return cached_call(
89 key=(id(self), 'get_description'),
90 validator=description_mtime,
91 producer=self._get_description
92 )
93
94 def _get_description(self):
9574 description = super(FancyRepo, self).get_description()
9675 if description:
9776 description = force_unicode(description)
10382 for prefix in ['refs/heads/', 'refs/tags/', '']:
10483 key = prefix + rev
10584 try:
106 obj = self[encode_for_git(key)]
85 obj = self.getref(encode_for_git(key))
10786 if isinstance(obj, dulwich.objects.Tag):
108 obj = self[obj.object[1]]
87 obj = self.getref(obj.object[1])
10988 return obj
11089 except KeyError:
11190 pass
128107 """Return a list of ref names that begin with `prefix`, ordered by the
129108 time they have been committed to last.
130109 """
131 def get_commit_time(refname):
132 try:
133 obj = self[refs[refname]]
134 except KeyError:
135 # Default to 0, i.e. sorting refs that point at non-existant
136 # objects last.
110 def get_commit_time(obj):
111 if obj is None:
112 # Put refs that point to non-existent objects last.
137113 return 0
138 if isinstance(obj, dulwich.objects.Tag):
114 elif isinstance(obj, dulwich.objects.Tag):
139115 return obj.tag_time
140 return obj.commit_time
141
142 refs = self.refs.as_dict(encode_for_git(prefix))
116 else:
117 return obj.commit_time
118
119 refs = self.get_resolved_refs_as_dict(
120 encode_for_git(prefix),
121 resolve_default=None
122 )
143123 if exclude:
144124 refs.pop(prefix + exclude, None)
145 sorted_names = sorted(refs.keys(), key=get_commit_time, reverse=True)
146 return [decode_from_git(ref) for ref in sorted_names]
125 sorted_refs = sorted(
126 refs.items(),
127 key=lambda item: get_commit_time(item[1]),
128 reverse=True
129 )
130 return [decode_from_git(name) for name, _ in sorted_refs]
147131
148132 def get_branch_names(self, exclude=None):
149133 """Return a list of branch names of this repo, ordered by the time they
157141
158142 def get_tag_and_branch_shas(self):
159143 """Return a list of SHAs of all tags and branches."""
160 tag_shas = self.refs.as_dict(b'refs/tags/').values()
161 branch_shas = self.refs.as_dict(b'refs/heads/').values()
144 tag_shas = self.get_refs_as_dict('refs/tags/').values()
145 branch_shas = self.get_refs_as_dict('refs/heads/').values()
162146 return set(tag_shas) | set(branch_shas)
163147
164148 def history(self, commit, path=None, max_commits=None, skip=0):
186170
187171 output = subprocess.check_output(cmd, cwd=os.path.abspath(self.path))
188172 sha1_sums = output.strip().split(b'\n')
189 return [self[sha1] for sha1 in sha1_sums]
173 return [self.getref(sha1) for sha1 in sha1_sums]
190174
191175 def blame(self, commit, path):
192176 """Return a 'git blame' list for the file at `path`: For each line in
196180 cmd = ['git', 'blame', '-ls', '--root', decode_from_git(commit.id), '--', path]
197181 output = subprocess.check_output(cmd, cwd=os.path.abspath(self.path))
198182 sha1_sums = [line[:40] for line in output.strip().split(b'\n')]
199 return [None if self[sha1] is None else decode_from_git(self[sha1].id) for sha1 in sha1_sums]
183 lines = []
184 for sha1 in sha1_sums:
185 obj = self.getref(sha1, None)
186 if obj is not None:
187 obj = decode_from_git(obj.id)
188 lines.append(obj)
189 return lines
200190
201191 def get_blob_or_tree(self, commit, path):
202192 """Return the Git tree or blob object for `path` at `commit`."""
203193 try:
204 (mode, oid) = tree_lookup_path(self.__getitem__, commit.tree,
194 (mode, oid) = tree_lookup_path(self.getref, commit.tree,
205195 encode_for_git(path))
206196 except NotTreeError:
207197 # Some part of the path was a file where a folder was expected.
208198 # Example: path="/path/to/foo.txt" but "to" is a file in "/path".
209199 raise KeyError
210 return self[oid]
200 return self.getref(oid)
211201
212202 def listdir(self, commit, path):
213203 """Return a list of submodules, directories and files in given
242232 from klaus.utils import guess_is_binary
243233
244234 if commit.parents:
245 parent_tree = self[commit.parents[0]].tree
235 parent_tree = self.getref(commit.parents[0]).tree
246236 else:
247237 parent_tree = None
248238
289279
290280 def raw_commit_diff(self, commit):
291281 if commit.parents:
292 parent_tree = self[commit.parents[0]].tree
282 parent_tree = self.getref(commit.parents[0]).tree
293283 else:
294284 parent_tree = None
295285 bytesio = io.BytesIO()
296286 dulwich.patch.write_tree_diff(bytesio, self.object_store, parent_tree, commit.tree)
297287 return bytesio.getvalue()
298288
299 def freeze(self):
300 return FrozenFancyRepo(self)
301
302
303 class FrozenFancyRepo(object):
304 """A special version of FancyRepo that assumes the underlying Git
305 repository does not change. Used for performance optimizations.
306 """
307 def __init__(self, repo):
308 self.__repo = repo
309 self.__last_updated_at = NOT_SET
310
311 def __setattr__(self, name, value):
312 if not name.startswith('_FrozenFancyRepo__'):
313 raise TypeError("Can't set %s attribute on FrozenFancyRepo" % name)
314 super(FrozenFancyRepo, self).__setattr__(name, value)
315
316 def __getattr__(self, name):
317 return getattr(self.__repo, name)
318
319 def fast_get_last_updated_at(self):
320 if self.__last_updated_at is NOT_SET:
321 self.__last_updated_at = self.__repo.get_last_updated_at()
322 return self.__last_updated_at
323
324289
325290 class InvalidRepo:
326291 """Represent an invalid repository and store pertinent data."""
1515 </h2>
1616 <ul class=repolist>
1717 {% for repo in repos %}
18 {% set last_updated_at = repo.fast_get_last_updated_at() %}
18 {% set last_updated_at = repo.get_last_updated_at() %}
1919 {% set description = repo.get_description() %}
2020 <li>
2121 <a
3636 sort_key = lambda repo: repo.name
3737 else:
3838 order_by = 'last_updated'
39 sort_key = lambda repo: (-(repo.fast_get_last_updated_at() or -1), repo.name)
40 repos = sorted([repo.freeze() for repo in current_app.valid_repos.values()],
41 key=sort_key)
39 sort_key = lambda repo: (-(repo.get_last_updated_at() or -1), repo.name)
40 repos = sorted(current_app.valid_repos.values(), key=sort_key)
4241 invalid_repos = sorted(current_app.invalid_repos.values(), key=lambda repo: repo.name)
4342 return render_template('repo_list.html', repos=repos, invalid_repos=invalid_repos,
4443 order_by=order_by, base_href=None)