16 | 16 |
NOT_SET = '__not_set__'
|
17 | 17 |
|
18 | 18 |
|
|
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 |
|
19 | 27 |
class FancyRepo(dulwich.repo.Repo):
|
20 | 28 |
"""A wrapper around Dulwich's Repo that adds some helper methods."""
|
21 | 29 |
@property
|
22 | 30 |
def name(self):
|
23 | 31 |
return repo_human_name(self.path)
|
24 | 32 |
|
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 | |
|
48 | 33 |
# TODO: factor out stuff into dulwich
|
49 | 34 |
def get_last_updated_at(self):
|
50 | 35 |
"""Get datetime of last commit to this repository."""
|
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
|
|
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
|
57 | 63 |
|
58 | 64 |
@property
|
59 | 65 |
def cloneurl(self):
|
|
71 | 77 |
"""Like Dulwich's `get_description`, but returns None if the file
|
72 | 78 |
contains Git's default text "Unnamed repository[...]".
|
73 | 79 |
"""
|
|
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):
|
74 | 95 |
description = super(FancyRepo, self).get_description()
|
75 | 96 |
if description:
|
76 | 97 |
description = force_unicode(description)
|
|
82 | 103 |
for prefix in ['refs/heads/', 'refs/tags/', '']:
|
83 | 104 |
key = prefix + rev
|
84 | 105 |
try:
|
85 | |
obj = self.getref(encode_for_git(key))
|
|
106 |
obj = self[encode_for_git(key)]
|
86 | 107 |
if isinstance(obj, dulwich.objects.Tag):
|
87 | |
obj = self.getref(obj.object[1])
|
|
108 |
obj = self[obj.object[1]]
|
88 | 109 |
return obj
|
89 | 110 |
except KeyError:
|
90 | 111 |
pass
|
|
107 | 128 |
"""Return a list of ref names that begin with `prefix`, ordered by the
|
108 | 129 |
time they have been committed to last.
|
109 | 130 |
"""
|
110 | |
def get_commit_time(obj):
|
111 | |
if obj is None:
|
112 | |
# Put refs that point to non-existent objects last.
|
|
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.
|
113 | 137 |
return 0
|
114 | |
elif isinstance(obj, dulwich.objects.Tag):
|
|
138 |
if isinstance(obj, dulwich.objects.Tag):
|
115 | 139 |
return obj.tag_time
|
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 | |
)
|
|
140 |
return obj.commit_time
|
|
141 |
|
|
142 |
refs = self.refs.as_dict(encode_for_git(prefix))
|
123 | 143 |
if exclude:
|
124 | 144 |
refs.pop(prefix + exclude, None)
|
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]
|
|
145 |
sorted_names = sorted(refs.keys(), key=get_commit_time, reverse=True)
|
|
146 |
return [decode_from_git(ref) for ref in sorted_names]
|
131 | 147 |
|
132 | 148 |
def get_branch_names(self, exclude=None):
|
133 | 149 |
"""Return a list of branch names of this repo, ordered by the time they
|
|
141 | 157 |
|
142 | 158 |
def get_tag_and_branch_shas(self):
|
143 | 159 |
"""Return a list of SHAs of all tags and branches."""
|
144 | |
tag_shas = self.get_refs_as_dict(b'refs/tags/').values()
|
145 | |
branch_shas = self.get_refs_as_dict(b'refs/heads/').values()
|
|
160 |
tag_shas = self.refs.as_dict(b'refs/tags/').values()
|
|
161 |
branch_shas = self.refs.as_dict(b'refs/heads/').values()
|
146 | 162 |
return set(tag_shas) | set(branch_shas)
|
147 | 163 |
|
148 | 164 |
def history(self, commit, path=None, max_commits=None, skip=0):
|
|
170 | 186 |
|
171 | 187 |
output = subprocess.check_output(cmd, cwd=os.path.abspath(self.path))
|
172 | 188 |
sha1_sums = output.strip().split(b'\n')
|
173 | |
return [self.getref(sha1) for sha1 in sha1_sums]
|
|
189 |
return [self[sha1] for sha1 in sha1_sums]
|
174 | 190 |
|
175 | 191 |
def blame(self, commit, path):
|
176 | 192 |
"""Return a 'git blame' list for the file at `path`: For each line in
|
|
180 | 196 |
cmd = ['git', 'blame', '-ls', '--root', decode_from_git(commit.id), '--', path]
|
181 | 197 |
output = subprocess.check_output(cmd, cwd=os.path.abspath(self.path))
|
182 | 198 |
sha1_sums = [line[:40] for line in output.strip().split(b'\n')]
|
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
|
|
199 |
return [None if self[sha1] is None else decode_from_git(self[sha1].id) for sha1 in sha1_sums]
|
190 | 200 |
|
191 | 201 |
def get_blob_or_tree(self, commit, path):
|
192 | 202 |
"""Return the Git tree or blob object for `path` at `commit`."""
|
193 | 203 |
try:
|
194 | |
(mode, oid) = tree_lookup_path(self.getref, commit.tree,
|
|
204 |
(mode, oid) = tree_lookup_path(self.__getitem__, commit.tree,
|
195 | 205 |
encode_for_git(path))
|
196 | 206 |
except NotTreeError:
|
197 | 207 |
# Some part of the path was a file where a folder was expected.
|
198 | 208 |
# Example: path="/path/to/foo.txt" but "to" is a file in "/path".
|
199 | 209 |
raise KeyError
|
200 | |
return self.getref(oid)
|
|
210 |
return self[oid]
|
201 | 211 |
|
202 | 212 |
def listdir(self, commit, path):
|
203 | 213 |
"""Return a list of submodules, directories and files in given
|
|
232 | 242 |
from klaus.utils import guess_is_binary
|
233 | 243 |
|
234 | 244 |
if commit.parents:
|
235 | |
parent_tree = self.getref(commit.parents[0]).tree
|
|
245 |
parent_tree = self[commit.parents[0]].tree
|
236 | 246 |
else:
|
237 | 247 |
parent_tree = None
|
238 | 248 |
|
|
279 | 289 |
|
280 | 290 |
def raw_commit_diff(self, commit):
|
281 | 291 |
if commit.parents:
|
282 | |
parent_tree = self.getref(commit.parents[0]).tree
|
|
292 |
parent_tree = self[commit.parents[0]].tree
|
283 | 293 |
else:
|
284 | 294 |
parent_tree = None
|
285 | 295 |
bytesio = io.BytesIO()
|
286 | 296 |
dulwich.patch.write_tree_diff(bytesio, self.object_store, parent_tree, commit.tree)
|
287 | 297 |
return bytesio.getvalue()
|
288 | 298 |
|
|
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 |
|
289 | 324 |
|
290 | 325 |
class InvalidRepo:
|
291 | 326 |
"""Represent an invalid repository and store pertinent data."""
|