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