##// END OF EJS Templates
vcs: added possibility to pre-load attributes for FileNodes.
marcink -
r1355:71f19ea6 default
parent child Browse files
Show More
@@ -1,527 +1,527 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 GIT commit module
22 GIT commit module
23 """
23 """
24
24
25 import re
25 import re
26 import stat
26 import stat
27 from ConfigParser import ConfigParser
27 from ConfigParser import ConfigParser
28 from itertools import chain
28 from itertools import chain
29 from StringIO import StringIO
29 from StringIO import StringIO
30
30
31 from zope.cachedescriptors.property import Lazy as LazyProperty
31 from zope.cachedescriptors.property import Lazy as LazyProperty
32
32
33 from rhodecode.lib.datelib import utcdate_fromtimestamp
33 from rhodecode.lib.datelib import utcdate_fromtimestamp
34 from rhodecode.lib.utils import safe_unicode, safe_str
34 from rhodecode.lib.utils import safe_unicode, safe_str
35 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.utils2 import safe_int
36 from rhodecode.lib.vcs.conf import settings
36 from rhodecode.lib.vcs.conf import settings
37 from rhodecode.lib.vcs.backends import base
37 from rhodecode.lib.vcs.backends import base
38 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
38 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
39 from rhodecode.lib.vcs.nodes import (
39 from rhodecode.lib.vcs.nodes import (
40 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
40 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
41 ChangedFileNodesGenerator, AddedFileNodesGenerator,
41 ChangedFileNodesGenerator, AddedFileNodesGenerator,
42 RemovedFileNodesGenerator)
42 RemovedFileNodesGenerator)
43
43
44
44
45 class GitCommit(base.BaseCommit):
45 class GitCommit(base.BaseCommit):
46 """
46 """
47 Represents state of the repository at single commit id.
47 Represents state of the repository at single commit id.
48 """
48 """
49 _author_property = 'author'
49 _author_property = 'author'
50 _committer_property = 'committer'
50 _committer_property = 'committer'
51 _date_property = 'commit_time'
51 _date_property = 'commit_time'
52 _date_tz_property = 'commit_timezone'
52 _date_tz_property = 'commit_timezone'
53 _message_property = 'message'
53 _message_property = 'message'
54 _parents_property = 'parents'
54 _parents_property = 'parents'
55
55
56 _filter_pre_load = [
56 _filter_pre_load = [
57 # done through a more complex tree walk on parents
57 # done through a more complex tree walk on parents
58 "affected_files",
58 "affected_files",
59 # based on repository cached property
59 # based on repository cached property
60 "branch",
60 "branch",
61 # done through subprocess not remote call
61 # done through subprocess not remote call
62 "children",
62 "children",
63 # done through a more complex tree walk on parents
63 # done through a more complex tree walk on parents
64 "status",
64 "status",
65 # mercurial specific property not supported here
65 # mercurial specific property not supported here
66 "_file_paths",
66 "_file_paths",
67 ]
67 ]
68
68
69 def __init__(self, repository, raw_id, idx, pre_load=None):
69 def __init__(self, repository, raw_id, idx, pre_load=None):
70 self.repository = repository
70 self.repository = repository
71 self._remote = repository._remote
71 self._remote = repository._remote
72 # TODO: johbo: Tweak of raw_id should not be necessary
72 # TODO: johbo: Tweak of raw_id should not be necessary
73 self.raw_id = safe_str(raw_id)
73 self.raw_id = safe_str(raw_id)
74 self.idx = idx
74 self.idx = idx
75
75
76 self._set_bulk_properties(pre_load)
76 self._set_bulk_properties(pre_load)
77
77
78 # caches
78 # caches
79 self._stat_modes = {} # stat info for paths
79 self._stat_modes = {} # stat info for paths
80 self._paths = {} # path processed with parse_tree
80 self._paths = {} # path processed with parse_tree
81 self.nodes = {}
81 self.nodes = {}
82 self._submodules = None
82 self._submodules = None
83
83
84 def _set_bulk_properties(self, pre_load):
84 def _set_bulk_properties(self, pre_load):
85 if not pre_load:
85 if not pre_load:
86 return
86 return
87 pre_load = [entry for entry in pre_load
87 pre_load = [entry for entry in pre_load
88 if entry not in self._filter_pre_load]
88 if entry not in self._filter_pre_load]
89 if not pre_load:
89 if not pre_load:
90 return
90 return
91
91
92 result = self._remote.bulk_request(self.raw_id, pre_load)
92 result = self._remote.bulk_request(self.raw_id, pre_load)
93 for attr, value in result.items():
93 for attr, value in result.items():
94 if attr in ["author", "message"]:
94 if attr in ["author", "message"]:
95 if value:
95 if value:
96 value = safe_unicode(value)
96 value = safe_unicode(value)
97 elif attr == "date":
97 elif attr == "date":
98 value = utcdate_fromtimestamp(*value)
98 value = utcdate_fromtimestamp(*value)
99 elif attr == "parents":
99 elif attr == "parents":
100 value = self._make_commits(value)
100 value = self._make_commits(value)
101 self.__dict__[attr] = value
101 self.__dict__[attr] = value
102
102
103 @LazyProperty
103 @LazyProperty
104 def _commit(self):
104 def _commit(self):
105 return self._remote[self.raw_id]
105 return self._remote[self.raw_id]
106
106
107 @LazyProperty
107 @LazyProperty
108 def _tree_id(self):
108 def _tree_id(self):
109 return self._remote[self._commit['tree']]['id']
109 return self._remote[self._commit['tree']]['id']
110
110
111 @LazyProperty
111 @LazyProperty
112 def id(self):
112 def id(self):
113 return self.raw_id
113 return self.raw_id
114
114
115 @LazyProperty
115 @LazyProperty
116 def short_id(self):
116 def short_id(self):
117 return self.raw_id[:12]
117 return self.raw_id[:12]
118
118
119 @LazyProperty
119 @LazyProperty
120 def message(self):
120 def message(self):
121 return safe_unicode(
121 return safe_unicode(
122 self._remote.commit_attribute(self.id, self._message_property))
122 self._remote.commit_attribute(self.id, self._message_property))
123
123
124 @LazyProperty
124 @LazyProperty
125 def committer(self):
125 def committer(self):
126 return safe_unicode(
126 return safe_unicode(
127 self._remote.commit_attribute(self.id, self._committer_property))
127 self._remote.commit_attribute(self.id, self._committer_property))
128
128
129 @LazyProperty
129 @LazyProperty
130 def author(self):
130 def author(self):
131 return safe_unicode(
131 return safe_unicode(
132 self._remote.commit_attribute(self.id, self._author_property))
132 self._remote.commit_attribute(self.id, self._author_property))
133
133
134 @LazyProperty
134 @LazyProperty
135 def date(self):
135 def date(self):
136 unix_ts, tz = self._remote.get_object_attrs(
136 unix_ts, tz = self._remote.get_object_attrs(
137 self.raw_id, self._date_property, self._date_tz_property)
137 self.raw_id, self._date_property, self._date_tz_property)
138 return utcdate_fromtimestamp(unix_ts, tz)
138 return utcdate_fromtimestamp(unix_ts, tz)
139
139
140 @LazyProperty
140 @LazyProperty
141 def status(self):
141 def status(self):
142 """
142 """
143 Returns modified, added, removed, deleted files for current commit
143 Returns modified, added, removed, deleted files for current commit
144 """
144 """
145 return self.changed, self.added, self.removed
145 return self.changed, self.added, self.removed
146
146
147 @LazyProperty
147 @LazyProperty
148 def tags(self):
148 def tags(self):
149 tags = [safe_unicode(name) for name,
149 tags = [safe_unicode(name) for name,
150 commit_id in self.repository.tags.iteritems()
150 commit_id in self.repository.tags.iteritems()
151 if commit_id == self.raw_id]
151 if commit_id == self.raw_id]
152 return tags
152 return tags
153
153
154 @LazyProperty
154 @LazyProperty
155 def branch(self):
155 def branch(self):
156 for name, commit_id in self.repository.branches.iteritems():
156 for name, commit_id in self.repository.branches.iteritems():
157 if commit_id == self.raw_id:
157 if commit_id == self.raw_id:
158 return safe_unicode(name)
158 return safe_unicode(name)
159 return None
159 return None
160
160
161 def _get_id_for_path(self, path):
161 def _get_id_for_path(self, path):
162 path = safe_str(path)
162 path = safe_str(path)
163 if path in self._paths:
163 if path in self._paths:
164 return self._paths[path]
164 return self._paths[path]
165
165
166 tree_id = self._tree_id
166 tree_id = self._tree_id
167
167
168 path = path.strip('/')
168 path = path.strip('/')
169 if path == '':
169 if path == '':
170 data = [tree_id, "tree"]
170 data = [tree_id, "tree"]
171 self._paths[''] = data
171 self._paths[''] = data
172 return data
172 return data
173
173
174 parts = path.split('/')
174 parts = path.split('/')
175 dirs, name = parts[:-1], parts[-1]
175 dirs, name = parts[:-1], parts[-1]
176 cur_dir = ''
176 cur_dir = ''
177
177
178 # initially extract things from root dir
178 # initially extract things from root dir
179 tree_items = self._remote.tree_items(tree_id)
179 tree_items = self._remote.tree_items(tree_id)
180 self._process_tree_items(tree_items, cur_dir)
180 self._process_tree_items(tree_items, cur_dir)
181
181
182 for dir in dirs:
182 for dir in dirs:
183 if cur_dir:
183 if cur_dir:
184 cur_dir = '/'.join((cur_dir, dir))
184 cur_dir = '/'.join((cur_dir, dir))
185 else:
185 else:
186 cur_dir = dir
186 cur_dir = dir
187 dir_id = None
187 dir_id = None
188 for item, stat_, id_, type_ in tree_items:
188 for item, stat_, id_, type_ in tree_items:
189 if item == dir:
189 if item == dir:
190 dir_id = id_
190 dir_id = id_
191 break
191 break
192 if dir_id:
192 if dir_id:
193 if type_ != "tree":
193 if type_ != "tree":
194 raise CommitError('%s is not a directory' % cur_dir)
194 raise CommitError('%s is not a directory' % cur_dir)
195 # update tree
195 # update tree
196 tree_items = self._remote.tree_items(dir_id)
196 tree_items = self._remote.tree_items(dir_id)
197 else:
197 else:
198 raise CommitError('%s have not been found' % cur_dir)
198 raise CommitError('%s have not been found' % cur_dir)
199
199
200 # cache all items from the given traversed tree
200 # cache all items from the given traversed tree
201 self._process_tree_items(tree_items, cur_dir)
201 self._process_tree_items(tree_items, cur_dir)
202
202
203 if path not in self._paths:
203 if path not in self._paths:
204 raise self.no_node_at_path(path)
204 raise self.no_node_at_path(path)
205
205
206 return self._paths[path]
206 return self._paths[path]
207
207
208 def _process_tree_items(self, items, cur_dir):
208 def _process_tree_items(self, items, cur_dir):
209 for item, stat_, id_, type_ in items:
209 for item, stat_, id_, type_ in items:
210 if cur_dir:
210 if cur_dir:
211 name = '/'.join((cur_dir, item))
211 name = '/'.join((cur_dir, item))
212 else:
212 else:
213 name = item
213 name = item
214 self._paths[name] = [id_, type_]
214 self._paths[name] = [id_, type_]
215 self._stat_modes[name] = stat_
215 self._stat_modes[name] = stat_
216
216
217 def _get_kind(self, path):
217 def _get_kind(self, path):
218 path_id, type_ = self._get_id_for_path(path)
218 path_id, type_ = self._get_id_for_path(path)
219 if type_ == 'blob':
219 if type_ == 'blob':
220 return NodeKind.FILE
220 return NodeKind.FILE
221 elif type_ == 'tree':
221 elif type_ == 'tree':
222 return NodeKind.DIR
222 return NodeKind.DIR
223 elif type == 'link':
223 elif type == 'link':
224 return NodeKind.SUBMODULE
224 return NodeKind.SUBMODULE
225 return None
225 return None
226
226
227 def _get_filectx(self, path):
227 def _get_filectx(self, path):
228 path = self._fix_path(path)
228 path = self._fix_path(path)
229 if self._get_kind(path) != NodeKind.FILE:
229 if self._get_kind(path) != NodeKind.FILE:
230 raise CommitError(
230 raise CommitError(
231 "File does not exist for commit %s at '%s'" %
231 "File does not exist for commit %s at '%s'" %
232 (self.raw_id, path))
232 (self.raw_id, path))
233 return path
233 return path
234
234
235 def _get_file_nodes(self):
235 def _get_file_nodes(self):
236 return chain(*(t[2] for t in self.walk()))
236 return chain(*(t[2] for t in self.walk()))
237
237
238 @LazyProperty
238 @LazyProperty
239 def parents(self):
239 def parents(self):
240 """
240 """
241 Returns list of parent commits.
241 Returns list of parent commits.
242 """
242 """
243 parent_ids = self._remote.commit_attribute(
243 parent_ids = self._remote.commit_attribute(
244 self.id, self._parents_property)
244 self.id, self._parents_property)
245 return self._make_commits(parent_ids)
245 return self._make_commits(parent_ids)
246
246
247 @LazyProperty
247 @LazyProperty
248 def children(self):
248 def children(self):
249 """
249 """
250 Returns list of child commits.
250 Returns list of child commits.
251 """
251 """
252 rev_filter = settings.GIT_REV_FILTER
252 rev_filter = settings.GIT_REV_FILTER
253 output, __ = self.repository.run_git_command(
253 output, __ = self.repository.run_git_command(
254 ['rev-list', '--children'] + rev_filter)
254 ['rev-list', '--children'] + rev_filter)
255
255
256 child_ids = []
256 child_ids = []
257 pat = re.compile(r'^%s' % self.raw_id)
257 pat = re.compile(r'^%s' % self.raw_id)
258 for l in output.splitlines():
258 for l in output.splitlines():
259 if pat.match(l):
259 if pat.match(l):
260 found_ids = l.split(' ')[1:]
260 found_ids = l.split(' ')[1:]
261 child_ids.extend(found_ids)
261 child_ids.extend(found_ids)
262 return self._make_commits(child_ids)
262 return self._make_commits(child_ids)
263
263
264 def _make_commits(self, commit_ids):
264 def _make_commits(self, commit_ids):
265 return [self.repository.get_commit(commit_id=commit_id)
265 return [self.repository.get_commit(commit_id=commit_id)
266 for commit_id in commit_ids]
266 for commit_id in commit_ids]
267
267
268 def get_file_mode(self, path):
268 def get_file_mode(self, path):
269 """
269 """
270 Returns stat mode of the file at the given `path`.
270 Returns stat mode of the file at the given `path`.
271 """
271 """
272 path = safe_str(path)
272 path = safe_str(path)
273 # ensure path is traversed
273 # ensure path is traversed
274 self._get_id_for_path(path)
274 self._get_id_for_path(path)
275 return self._stat_modes[path]
275 return self._stat_modes[path]
276
276
277 def is_link(self, path):
277 def is_link(self, path):
278 return stat.S_ISLNK(self.get_file_mode(path))
278 return stat.S_ISLNK(self.get_file_mode(path))
279
279
280 def get_file_content(self, path):
280 def get_file_content(self, path):
281 """
281 """
282 Returns content of the file at given `path`.
282 Returns content of the file at given `path`.
283 """
283 """
284 id_, _ = self._get_id_for_path(path)
284 id_, _ = self._get_id_for_path(path)
285 return self._remote.blob_as_pretty_string(id_)
285 return self._remote.blob_as_pretty_string(id_)
286
286
287 def get_file_size(self, path):
287 def get_file_size(self, path):
288 """
288 """
289 Returns size of the file at given `path`.
289 Returns size of the file at given `path`.
290 """
290 """
291 id_, _ = self._get_id_for_path(path)
291 id_, _ = self._get_id_for_path(path)
292 return self._remote.blob_raw_length(id_)
292 return self._remote.blob_raw_length(id_)
293
293
294 def get_file_history(self, path, limit=None, pre_load=None):
294 def get_file_history(self, path, limit=None, pre_load=None):
295 """
295 """
296 Returns history of file as reversed list of `GitCommit` objects for
296 Returns history of file as reversed list of `GitCommit` objects for
297 which file at given `path` has been modified.
297 which file at given `path` has been modified.
298
298
299 TODO: This function now uses an underlying 'git' command which works
299 TODO: This function now uses an underlying 'git' command which works
300 quickly but ideally we should replace with an algorithm.
300 quickly but ideally we should replace with an algorithm.
301 """
301 """
302 self._get_filectx(path)
302 self._get_filectx(path)
303 f_path = safe_str(path)
303 f_path = safe_str(path)
304
304
305 cmd = ['log']
305 cmd = ['log']
306 if limit:
306 if limit:
307 cmd.extend(['-n', str(safe_int(limit, 0))])
307 cmd.extend(['-n', str(safe_int(limit, 0))])
308 cmd.extend(['--pretty=format: %H', '-s', self.raw_id, '--', f_path])
308 cmd.extend(['--pretty=format: %H', '-s', self.raw_id, '--', f_path])
309
309
310 output, __ = self.repository.run_git_command(cmd)
310 output, __ = self.repository.run_git_command(cmd)
311 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
311 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
312
312
313 return [
313 return [
314 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
314 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
315 for commit_id in commit_ids]
315 for commit_id in commit_ids]
316
316
317 # TODO: unused for now potential replacement for subprocess
317 # TODO: unused for now potential replacement for subprocess
318 def get_file_history_2(self, path, limit=None, pre_load=None):
318 def get_file_history_2(self, path, limit=None, pre_load=None):
319 """
319 """
320 Returns history of file as reversed list of `Commit` objects for
320 Returns history of file as reversed list of `Commit` objects for
321 which file at given `path` has been modified.
321 which file at given `path` has been modified.
322 """
322 """
323 self._get_filectx(path)
323 self._get_filectx(path)
324 f_path = safe_str(path)
324 f_path = safe_str(path)
325
325
326 commit_ids = self._remote.get_file_history(f_path, self.id, limit)
326 commit_ids = self._remote.get_file_history(f_path, self.id, limit)
327
327
328 return [
328 return [
329 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
329 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
330 for commit_id in commit_ids]
330 for commit_id in commit_ids]
331
331
332 def get_file_annotate(self, path, pre_load=None):
332 def get_file_annotate(self, path, pre_load=None):
333 """
333 """
334 Returns a generator of four element tuples with
334 Returns a generator of four element tuples with
335 lineno, commit_id, commit lazy loader and line
335 lineno, commit_id, commit lazy loader and line
336
336
337 TODO: This function now uses os underlying 'git' command which is
337 TODO: This function now uses os underlying 'git' command which is
338 generally not good. Should be replaced with algorithm iterating
338 generally not good. Should be replaced with algorithm iterating
339 commits.
339 commits.
340 """
340 """
341 cmd = ['blame', '-l', '--root', '-r', self.raw_id, '--', path]
341 cmd = ['blame', '-l', '--root', '-r', self.raw_id, '--', path]
342 # -l ==> outputs long shas (and we need all 40 characters)
342 # -l ==> outputs long shas (and we need all 40 characters)
343 # --root ==> doesn't put '^' character for bounderies
343 # --root ==> doesn't put '^' character for bounderies
344 # -r commit_id ==> blames for the given commit
344 # -r commit_id ==> blames for the given commit
345 output, __ = self.repository.run_git_command(cmd)
345 output, __ = self.repository.run_git_command(cmd)
346
346
347 for i, blame_line in enumerate(output.split('\n')[:-1]):
347 for i, blame_line in enumerate(output.split('\n')[:-1]):
348 line_no = i + 1
348 line_no = i + 1
349 commit_id, line = re.split(r' ', blame_line, 1)
349 commit_id, line = re.split(r' ', blame_line, 1)
350 yield (
350 yield (
351 line_no, commit_id,
351 line_no, commit_id,
352 lambda: self.repository.get_commit(commit_id=commit_id,
352 lambda: self.repository.get_commit(commit_id=commit_id,
353 pre_load=pre_load),
353 pre_load=pre_load),
354 line)
354 line)
355
355
356 def get_nodes(self, path):
356 def get_nodes(self, path):
357 if self._get_kind(path) != NodeKind.DIR:
357 if self._get_kind(path) != NodeKind.DIR:
358 raise CommitError(
358 raise CommitError(
359 "Directory does not exist for commit %s at "
359 "Directory does not exist for commit %s at "
360 " '%s'" % (self.raw_id, path))
360 " '%s'" % (self.raw_id, path))
361 path = self._fix_path(path)
361 path = self._fix_path(path)
362 id_, _ = self._get_id_for_path(path)
362 id_, _ = self._get_id_for_path(path)
363 tree_id = self._remote[id_]['id']
363 tree_id = self._remote[id_]['id']
364 dirnodes = []
364 dirnodes = []
365 filenodes = []
365 filenodes = []
366 alias = self.repository.alias
366 alias = self.repository.alias
367 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
367 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
368 if type_ == 'link':
368 if type_ == 'link':
369 url = self._get_submodule_url('/'.join((path, name)))
369 url = self._get_submodule_url('/'.join((path, name)))
370 dirnodes.append(SubModuleNode(
370 dirnodes.append(SubModuleNode(
371 name, url=url, commit=id_, alias=alias))
371 name, url=url, commit=id_, alias=alias))
372 continue
372 continue
373
373
374 if path != '':
374 if path != '':
375 obj_path = '/'.join((path, name))
375 obj_path = '/'.join((path, name))
376 else:
376 else:
377 obj_path = name
377 obj_path = name
378 if obj_path not in self._stat_modes:
378 if obj_path not in self._stat_modes:
379 self._stat_modes[obj_path] = stat_
379 self._stat_modes[obj_path] = stat_
380
380
381 if type_ == 'tree':
381 if type_ == 'tree':
382 dirnodes.append(DirNode(obj_path, commit=self))
382 dirnodes.append(DirNode(obj_path, commit=self))
383 elif type_ == 'blob':
383 elif type_ == 'blob':
384 filenodes.append(FileNode(obj_path, commit=self, mode=stat_))
384 filenodes.append(FileNode(obj_path, commit=self, mode=stat_))
385 else:
385 else:
386 raise CommitError(
386 raise CommitError(
387 "Requested object should be Tree or Blob, is %s", type_)
387 "Requested object should be Tree or Blob, is %s", type_)
388
388
389 nodes = dirnodes + filenodes
389 nodes = dirnodes + filenodes
390 for node in nodes:
390 for node in nodes:
391 if node.path not in self.nodes:
391 if node.path not in self.nodes:
392 self.nodes[node.path] = node
392 self.nodes[node.path] = node
393 nodes.sort()
393 nodes.sort()
394 return nodes
394 return nodes
395
395
396 def get_node(self, path):
396 def get_node(self, path, pre_load=None):
397 if isinstance(path, unicode):
397 if isinstance(path, unicode):
398 path = path.encode('utf-8')
398 path = path.encode('utf-8')
399 path = self._fix_path(path)
399 path = self._fix_path(path)
400 if path not in self.nodes:
400 if path not in self.nodes:
401 try:
401 try:
402 id_, type_ = self._get_id_for_path(path)
402 id_, type_ = self._get_id_for_path(path)
403 except CommitError:
403 except CommitError:
404 raise NodeDoesNotExistError(
404 raise NodeDoesNotExistError(
405 "Cannot find one of parents' directories for a given "
405 "Cannot find one of parents' directories for a given "
406 "path: %s" % path)
406 "path: %s" % path)
407
407
408 if type_ == 'link':
408 if type_ == 'link':
409 url = self._get_submodule_url(path)
409 url = self._get_submodule_url(path)
410 node = SubModuleNode(path, url=url, commit=id_,
410 node = SubModuleNode(path, url=url, commit=id_,
411 alias=self.repository.alias)
411 alias=self.repository.alias)
412 elif type_ == 'tree':
412 elif type_ == 'tree':
413 if path == '':
413 if path == '':
414 node = RootNode(commit=self)
414 node = RootNode(commit=self)
415 else:
415 else:
416 node = DirNode(path, commit=self)
416 node = DirNode(path, commit=self)
417 elif type_ == 'blob':
417 elif type_ == 'blob':
418 node = FileNode(path, commit=self)
418 node = FileNode(path, commit=self, pre_load=pre_load)
419 else:
419 else:
420 raise self.no_node_at_path(path)
420 raise self.no_node_at_path(path)
421
421
422 # cache node
422 # cache node
423 self.nodes[path] = node
423 self.nodes[path] = node
424 return self.nodes[path]
424 return self.nodes[path]
425
425
426 @LazyProperty
426 @LazyProperty
427 def affected_files(self):
427 def affected_files(self):
428 """
428 """
429 Gets a fast accessible file changes for given commit
429 Gets a fast accessible file changes for given commit
430 """
430 """
431 added, modified, deleted = self._changes_cache
431 added, modified, deleted = self._changes_cache
432 return list(added.union(modified).union(deleted))
432 return list(added.union(modified).union(deleted))
433
433
434 @LazyProperty
434 @LazyProperty
435 def _changes_cache(self):
435 def _changes_cache(self):
436 added = set()
436 added = set()
437 modified = set()
437 modified = set()
438 deleted = set()
438 deleted = set()
439 _r = self._remote
439 _r = self._remote
440
440
441 parents = self.parents
441 parents = self.parents
442 if not self.parents:
442 if not self.parents:
443 parents = [base.EmptyCommit()]
443 parents = [base.EmptyCommit()]
444 for parent in parents:
444 for parent in parents:
445 if isinstance(parent, base.EmptyCommit):
445 if isinstance(parent, base.EmptyCommit):
446 oid = None
446 oid = None
447 else:
447 else:
448 oid = parent.raw_id
448 oid = parent.raw_id
449 changes = _r.tree_changes(oid, self.raw_id)
449 changes = _r.tree_changes(oid, self.raw_id)
450 for (oldpath, newpath), (_, _), (_, _) in changes:
450 for (oldpath, newpath), (_, _), (_, _) in changes:
451 if newpath and oldpath:
451 if newpath and oldpath:
452 modified.add(newpath)
452 modified.add(newpath)
453 elif newpath and not oldpath:
453 elif newpath and not oldpath:
454 added.add(newpath)
454 added.add(newpath)
455 elif not newpath and oldpath:
455 elif not newpath and oldpath:
456 deleted.add(oldpath)
456 deleted.add(oldpath)
457 return added, modified, deleted
457 return added, modified, deleted
458
458
459 def _get_paths_for_status(self, status):
459 def _get_paths_for_status(self, status):
460 """
460 """
461 Returns sorted list of paths for given ``status``.
461 Returns sorted list of paths for given ``status``.
462
462
463 :param status: one of: *added*, *modified* or *deleted*
463 :param status: one of: *added*, *modified* or *deleted*
464 """
464 """
465 added, modified, deleted = self._changes_cache
465 added, modified, deleted = self._changes_cache
466 return sorted({
466 return sorted({
467 'added': list(added),
467 'added': list(added),
468 'modified': list(modified),
468 'modified': list(modified),
469 'deleted': list(deleted)}[status]
469 'deleted': list(deleted)}[status]
470 )
470 )
471
471
472 @LazyProperty
472 @LazyProperty
473 def added(self):
473 def added(self):
474 """
474 """
475 Returns list of added ``FileNode`` objects.
475 Returns list of added ``FileNode`` objects.
476 """
476 """
477 if not self.parents:
477 if not self.parents:
478 return list(self._get_file_nodes())
478 return list(self._get_file_nodes())
479 return AddedFileNodesGenerator(
479 return AddedFileNodesGenerator(
480 [n for n in self._get_paths_for_status('added')], self)
480 [n for n in self._get_paths_for_status('added')], self)
481
481
482 @LazyProperty
482 @LazyProperty
483 def changed(self):
483 def changed(self):
484 """
484 """
485 Returns list of modified ``FileNode`` objects.
485 Returns list of modified ``FileNode`` objects.
486 """
486 """
487 if not self.parents:
487 if not self.parents:
488 return []
488 return []
489 return ChangedFileNodesGenerator(
489 return ChangedFileNodesGenerator(
490 [n for n in self._get_paths_for_status('modified')], self)
490 [n for n in self._get_paths_for_status('modified')], self)
491
491
492 @LazyProperty
492 @LazyProperty
493 def removed(self):
493 def removed(self):
494 """
494 """
495 Returns list of removed ``FileNode`` objects.
495 Returns list of removed ``FileNode`` objects.
496 """
496 """
497 if not self.parents:
497 if not self.parents:
498 return []
498 return []
499 return RemovedFileNodesGenerator(
499 return RemovedFileNodesGenerator(
500 [n for n in self._get_paths_for_status('deleted')], self)
500 [n for n in self._get_paths_for_status('deleted')], self)
501
501
502 def _get_submodule_url(self, submodule_path):
502 def _get_submodule_url(self, submodule_path):
503 git_modules_path = '.gitmodules'
503 git_modules_path = '.gitmodules'
504
504
505 if self._submodules is None:
505 if self._submodules is None:
506 self._submodules = {}
506 self._submodules = {}
507
507
508 try:
508 try:
509 submodules_node = self.get_node(git_modules_path)
509 submodules_node = self.get_node(git_modules_path)
510 except NodeDoesNotExistError:
510 except NodeDoesNotExistError:
511 return None
511 return None
512
512
513 content = submodules_node.content
513 content = submodules_node.content
514
514
515 # ConfigParser fails if there are whitespaces
515 # ConfigParser fails if there are whitespaces
516 content = '\n'.join(l.strip() for l in content.split('\n'))
516 content = '\n'.join(l.strip() for l in content.split('\n'))
517
517
518 parser = ConfigParser()
518 parser = ConfigParser()
519 parser.readfp(StringIO(content))
519 parser.readfp(StringIO(content))
520
520
521 for section in parser.sections():
521 for section in parser.sections():
522 path = parser.get(section, 'path')
522 path = parser.get(section, 'path')
523 url = parser.get(section, 'url')
523 url = parser.get(section, 'url')
524 if path and url:
524 if path and url:
525 self._submodules[path.strip('/')] = url
525 self._submodules[path.strip('/')] = url
526
526
527 return self._submodules.get(submodule_path.strip('/'))
527 return self._submodules.get(submodule_path.strip('/'))
@@ -1,362 +1,362 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 HG commit module
22 HG commit module
23 """
23 """
24
24
25 import os
25 import os
26
26
27 from zope.cachedescriptors.property import Lazy as LazyProperty
27 from zope.cachedescriptors.property import Lazy as LazyProperty
28
28
29 from rhodecode.lib.datelib import utcdate_fromtimestamp
29 from rhodecode.lib.datelib import utcdate_fromtimestamp
30 from rhodecode.lib.utils import safe_str, safe_unicode
30 from rhodecode.lib.utils import safe_str, safe_unicode
31 from rhodecode.lib.vcs import path as vcspath
31 from rhodecode.lib.vcs import path as vcspath
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
34 from rhodecode.lib.vcs.exceptions import CommitError
34 from rhodecode.lib.vcs.exceptions import CommitError
35 from rhodecode.lib.vcs.nodes import (
35 from rhodecode.lib.vcs.nodes import (
36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
38 LargeFileNode, LARGEFILE_PREFIX)
38 LargeFileNode, LARGEFILE_PREFIX)
39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
40
40
41
41
42 class MercurialCommit(base.BaseCommit):
42 class MercurialCommit(base.BaseCommit):
43 """
43 """
44 Represents state of the repository at the single commit.
44 Represents state of the repository at the single commit.
45 """
45 """
46
46
47 _filter_pre_load = [
47 _filter_pre_load = [
48 # git specific property not supported here
48 # git specific property not supported here
49 "_commit",
49 "_commit",
50 ]
50 ]
51
51
52 def __init__(self, repository, raw_id, idx, pre_load=None):
52 def __init__(self, repository, raw_id, idx, pre_load=None):
53 raw_id = safe_str(raw_id)
53 raw_id = safe_str(raw_id)
54
54
55 self.repository = repository
55 self.repository = repository
56 self._remote = repository._remote
56 self._remote = repository._remote
57
57
58 self.raw_id = raw_id
58 self.raw_id = raw_id
59 self.idx = repository._sanitize_commit_idx(idx)
59 self.idx = repository._sanitize_commit_idx(idx)
60
60
61 self._set_bulk_properties(pre_load)
61 self._set_bulk_properties(pre_load)
62
62
63 # caches
63 # caches
64 self.nodes = {}
64 self.nodes = {}
65
65
66 def _set_bulk_properties(self, pre_load):
66 def _set_bulk_properties(self, pre_load):
67 if not pre_load:
67 if not pre_load:
68 return
68 return
69 pre_load = [entry for entry in pre_load
69 pre_load = [entry for entry in pre_load
70 if entry not in self._filter_pre_load]
70 if entry not in self._filter_pre_load]
71 if not pre_load:
71 if not pre_load:
72 return
72 return
73
73
74 result = self._remote.bulk_request(self.idx, pre_load)
74 result = self._remote.bulk_request(self.idx, pre_load)
75 for attr, value in result.items():
75 for attr, value in result.items():
76 if attr in ["author", "branch", "message"]:
76 if attr in ["author", "branch", "message"]:
77 value = safe_unicode(value)
77 value = safe_unicode(value)
78 elif attr == "affected_files":
78 elif attr == "affected_files":
79 value = map(safe_unicode, value)
79 value = map(safe_unicode, value)
80 elif attr == "date":
80 elif attr == "date":
81 value = utcdate_fromtimestamp(*value)
81 value = utcdate_fromtimestamp(*value)
82 elif attr in ["children", "parents"]:
82 elif attr in ["children", "parents"]:
83 value = self._make_commits(value)
83 value = self._make_commits(value)
84 self.__dict__[attr] = value
84 self.__dict__[attr] = value
85
85
86 @LazyProperty
86 @LazyProperty
87 def tags(self):
87 def tags(self):
88 tags = [name for name, commit_id in self.repository.tags.iteritems()
88 tags = [name for name, commit_id in self.repository.tags.iteritems()
89 if commit_id == self.raw_id]
89 if commit_id == self.raw_id]
90 return tags
90 return tags
91
91
92 @LazyProperty
92 @LazyProperty
93 def branch(self):
93 def branch(self):
94 return safe_unicode(self._remote.ctx_branch(self.idx))
94 return safe_unicode(self._remote.ctx_branch(self.idx))
95
95
96 @LazyProperty
96 @LazyProperty
97 def bookmarks(self):
97 def bookmarks(self):
98 bookmarks = [
98 bookmarks = [
99 name for name, commit_id in self.repository.bookmarks.iteritems()
99 name for name, commit_id in self.repository.bookmarks.iteritems()
100 if commit_id == self.raw_id]
100 if commit_id == self.raw_id]
101 return bookmarks
101 return bookmarks
102
102
103 @LazyProperty
103 @LazyProperty
104 def message(self):
104 def message(self):
105 return safe_unicode(self._remote.ctx_description(self.idx))
105 return safe_unicode(self._remote.ctx_description(self.idx))
106
106
107 @LazyProperty
107 @LazyProperty
108 def committer(self):
108 def committer(self):
109 return safe_unicode(self.author)
109 return safe_unicode(self.author)
110
110
111 @LazyProperty
111 @LazyProperty
112 def author(self):
112 def author(self):
113 return safe_unicode(self._remote.ctx_user(self.idx))
113 return safe_unicode(self._remote.ctx_user(self.idx))
114
114
115 @LazyProperty
115 @LazyProperty
116 def date(self):
116 def date(self):
117 return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx))
117 return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx))
118
118
119 @LazyProperty
119 @LazyProperty
120 def status(self):
120 def status(self):
121 """
121 """
122 Returns modified, added, removed, deleted files for current commit
122 Returns modified, added, removed, deleted files for current commit
123 """
123 """
124 return self._remote.ctx_status(self.idx)
124 return self._remote.ctx_status(self.idx)
125
125
126 @LazyProperty
126 @LazyProperty
127 def _file_paths(self):
127 def _file_paths(self):
128 return self._remote.ctx_list(self.idx)
128 return self._remote.ctx_list(self.idx)
129
129
130 @LazyProperty
130 @LazyProperty
131 def _dir_paths(self):
131 def _dir_paths(self):
132 p = list(set(get_dirs_for_path(*self._file_paths)))
132 p = list(set(get_dirs_for_path(*self._file_paths)))
133 p.insert(0, '')
133 p.insert(0, '')
134 return p
134 return p
135
135
136 @LazyProperty
136 @LazyProperty
137 def _paths(self):
137 def _paths(self):
138 return self._dir_paths + self._file_paths
138 return self._dir_paths + self._file_paths
139
139
140 @LazyProperty
140 @LazyProperty
141 def id(self):
141 def id(self):
142 if self.last:
142 if self.last:
143 return u'tip'
143 return u'tip'
144 return self.short_id
144 return self.short_id
145
145
146 @LazyProperty
146 @LazyProperty
147 def short_id(self):
147 def short_id(self):
148 return self.raw_id[:12]
148 return self.raw_id[:12]
149
149
150 def _make_commits(self, indexes):
150 def _make_commits(self, indexes):
151 return [self.repository.get_commit(commit_idx=idx)
151 return [self.repository.get_commit(commit_idx=idx)
152 for idx in indexes if idx >= 0]
152 for idx in indexes if idx >= 0]
153
153
154 @LazyProperty
154 @LazyProperty
155 def parents(self):
155 def parents(self):
156 """
156 """
157 Returns list of parent commits.
157 Returns list of parent commits.
158 """
158 """
159 parents = self._remote.ctx_parents(self.idx)
159 parents = self._remote.ctx_parents(self.idx)
160 return self._make_commits(parents)
160 return self._make_commits(parents)
161
161
162 @LazyProperty
162 @LazyProperty
163 def children(self):
163 def children(self):
164 """
164 """
165 Returns list of child commits.
165 Returns list of child commits.
166 """
166 """
167 children = self._remote.ctx_children(self.idx)
167 children = self._remote.ctx_children(self.idx)
168 return self._make_commits(children)
168 return self._make_commits(children)
169
169
170 def diff(self, ignore_whitespace=True, context=3):
170 def diff(self, ignore_whitespace=True, context=3):
171 result = self._remote.ctx_diff(
171 result = self._remote.ctx_diff(
172 self.idx,
172 self.idx,
173 git=True, ignore_whitespace=ignore_whitespace, context=context)
173 git=True, ignore_whitespace=ignore_whitespace, context=context)
174 diff = ''.join(result)
174 diff = ''.join(result)
175 return MercurialDiff(diff)
175 return MercurialDiff(diff)
176
176
177 def _fix_path(self, path):
177 def _fix_path(self, path):
178 """
178 """
179 Mercurial keeps filenodes as str so we need to encode from unicode
179 Mercurial keeps filenodes as str so we need to encode from unicode
180 to str.
180 to str.
181 """
181 """
182 return safe_str(super(MercurialCommit, self)._fix_path(path))
182 return safe_str(super(MercurialCommit, self)._fix_path(path))
183
183
184 def _get_kind(self, path):
184 def _get_kind(self, path):
185 path = self._fix_path(path)
185 path = self._fix_path(path)
186 if path in self._file_paths:
186 if path in self._file_paths:
187 return NodeKind.FILE
187 return NodeKind.FILE
188 elif path in self._dir_paths:
188 elif path in self._dir_paths:
189 return NodeKind.DIR
189 return NodeKind.DIR
190 else:
190 else:
191 raise CommitError(
191 raise CommitError(
192 "Node does not exist at the given path '%s'" % (path, ))
192 "Node does not exist at the given path '%s'" % (path, ))
193
193
194 def _get_filectx(self, path):
194 def _get_filectx(self, path):
195 path = self._fix_path(path)
195 path = self._fix_path(path)
196 if self._get_kind(path) != NodeKind.FILE:
196 if self._get_kind(path) != NodeKind.FILE:
197 raise CommitError(
197 raise CommitError(
198 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
198 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
199 return path
199 return path
200
200
201 def get_file_mode(self, path):
201 def get_file_mode(self, path):
202 """
202 """
203 Returns stat mode of the file at the given ``path``.
203 Returns stat mode of the file at the given ``path``.
204 """
204 """
205 path = self._get_filectx(path)
205 path = self._get_filectx(path)
206 if 'x' in self._remote.fctx_flags(self.idx, path):
206 if 'x' in self._remote.fctx_flags(self.idx, path):
207 return base.FILEMODE_EXECUTABLE
207 return base.FILEMODE_EXECUTABLE
208 else:
208 else:
209 return base.FILEMODE_DEFAULT
209 return base.FILEMODE_DEFAULT
210
210
211 def is_link(self, path):
211 def is_link(self, path):
212 path = self._get_filectx(path)
212 path = self._get_filectx(path)
213 return 'l' in self._remote.fctx_flags(self.idx, path)
213 return 'l' in self._remote.fctx_flags(self.idx, path)
214
214
215 def get_file_content(self, path):
215 def get_file_content(self, path):
216 """
216 """
217 Returns content of the file at given ``path``.
217 Returns content of the file at given ``path``.
218 """
218 """
219 path = self._get_filectx(path)
219 path = self._get_filectx(path)
220 return self._remote.fctx_data(self.idx, path)
220 return self._remote.fctx_data(self.idx, path)
221
221
222 def get_file_size(self, path):
222 def get_file_size(self, path):
223 """
223 """
224 Returns size of the file at given ``path``.
224 Returns size of the file at given ``path``.
225 """
225 """
226 path = self._get_filectx(path)
226 path = self._get_filectx(path)
227 return self._remote.fctx_size(self.idx, path)
227 return self._remote.fctx_size(self.idx, path)
228
228
229 def get_file_history(self, path, limit=None, pre_load=None):
229 def get_file_history(self, path, limit=None, pre_load=None):
230 """
230 """
231 Returns history of file as reversed list of `MercurialCommit` objects
231 Returns history of file as reversed list of `MercurialCommit` objects
232 for which file at given ``path`` has been modified.
232 for which file at given ``path`` has been modified.
233 """
233 """
234 path = self._get_filectx(path)
234 path = self._get_filectx(path)
235 hist = self._remote.file_history(self.idx, path, limit)
235 hist = self._remote.file_history(self.idx, path, limit)
236 return [
236 return [
237 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
237 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
238 for commit_id in hist]
238 for commit_id in hist]
239
239
240 def get_file_annotate(self, path, pre_load=None):
240 def get_file_annotate(self, path, pre_load=None):
241 """
241 """
242 Returns a generator of four element tuples with
242 Returns a generator of four element tuples with
243 lineno, commit_id, commit lazy loader and line
243 lineno, commit_id, commit lazy loader and line
244 """
244 """
245 result = self._remote.fctx_annotate(self.idx, path)
245 result = self._remote.fctx_annotate(self.idx, path)
246
246
247 for ln_no, commit_id, content in result:
247 for ln_no, commit_id, content in result:
248 yield (
248 yield (
249 ln_no, commit_id,
249 ln_no, commit_id,
250 lambda: self.repository.get_commit(commit_id=commit_id,
250 lambda: self.repository.get_commit(commit_id=commit_id,
251 pre_load=pre_load),
251 pre_load=pre_load),
252 content)
252 content)
253
253
254 def get_nodes(self, path):
254 def get_nodes(self, path):
255 """
255 """
256 Returns combined ``DirNode`` and ``FileNode`` objects list representing
256 Returns combined ``DirNode`` and ``FileNode`` objects list representing
257 state of commit at the given ``path``. If node at the given ``path``
257 state of commit at the given ``path``. If node at the given ``path``
258 is not instance of ``DirNode``, CommitError would be raised.
258 is not instance of ``DirNode``, CommitError would be raised.
259 """
259 """
260
260
261 if self._get_kind(path) != NodeKind.DIR:
261 if self._get_kind(path) != NodeKind.DIR:
262 raise CommitError(
262 raise CommitError(
263 "Directory does not exist for idx %s at '%s'" %
263 "Directory does not exist for idx %s at '%s'" %
264 (self.idx, path))
264 (self.idx, path))
265 path = self._fix_path(path)
265 path = self._fix_path(path)
266
266
267 filenodes = [
267 filenodes = [
268 FileNode(f, commit=self) for f in self._file_paths
268 FileNode(f, commit=self) for f in self._file_paths
269 if os.path.dirname(f) == path]
269 if os.path.dirname(f) == path]
270 # TODO: johbo: Check if this can be done in a more obvious way
270 # TODO: johbo: Check if this can be done in a more obvious way
271 dirs = path == '' and '' or [
271 dirs = path == '' and '' or [
272 d for d in self._dir_paths
272 d for d in self._dir_paths
273 if d and vcspath.dirname(d) == path]
273 if d and vcspath.dirname(d) == path]
274 dirnodes = [
274 dirnodes = [
275 DirNode(d, commit=self) for d in dirs
275 DirNode(d, commit=self) for d in dirs
276 if os.path.dirname(d) == path]
276 if os.path.dirname(d) == path]
277
277
278 alias = self.repository.alias
278 alias = self.repository.alias
279 for k, vals in self._submodules.iteritems():
279 for k, vals in self._submodules.iteritems():
280 loc = vals[0]
280 loc = vals[0]
281 commit = vals[1]
281 commit = vals[1]
282 dirnodes.append(
282 dirnodes.append(
283 SubModuleNode(k, url=loc, commit=commit, alias=alias))
283 SubModuleNode(k, url=loc, commit=commit, alias=alias))
284 nodes = dirnodes + filenodes
284 nodes = dirnodes + filenodes
285 # cache nodes
285 # cache nodes
286 for node in nodes:
286 for node in nodes:
287 self.nodes[node.path] = node
287 self.nodes[node.path] = node
288 nodes.sort()
288 nodes.sort()
289
289
290 return nodes
290 return nodes
291
291
292 def get_node(self, path):
292 def get_node(self, path, pre_load=None):
293 """
293 """
294 Returns `Node` object from the given `path`. If there is no node at
294 Returns `Node` object from the given `path`. If there is no node at
295 the given `path`, `NodeDoesNotExistError` would be raised.
295 the given `path`, `NodeDoesNotExistError` would be raised.
296 """
296 """
297 path = self._fix_path(path)
297 path = self._fix_path(path)
298
298
299 if path not in self.nodes:
299 if path not in self.nodes:
300 if path in self._file_paths:
300 if path in self._file_paths:
301 node = FileNode(path, commit=self)
301 node = FileNode(path, commit=self, pre_load=pre_load)
302 elif path in self._dir_paths:
302 elif path in self._dir_paths:
303 if path == '':
303 if path == '':
304 node = RootNode(commit=self)
304 node = RootNode(commit=self)
305 else:
305 else:
306 node = DirNode(path, commit=self)
306 node = DirNode(path, commit=self)
307 else:
307 else:
308 raise self.no_node_at_path(path)
308 raise self.no_node_at_path(path)
309
309
310 # cache node
310 # cache node
311 self.nodes[path] = node
311 self.nodes[path] = node
312 return self.nodes[path]
312 return self.nodes[path]
313
313
314 def get_largefile_node(self, path):
314 def get_largefile_node(self, path):
315 path = os.path.join(LARGEFILE_PREFIX, path)
315 path = os.path.join(LARGEFILE_PREFIX, path)
316
316
317 if self._remote.is_large_file(path):
317 if self._remote.is_large_file(path):
318 # content of that file regular FileNode is the hash of largefile
318 # content of that file regular FileNode is the hash of largefile
319 file_id = self.get_file_content(path).strip()
319 file_id = self.get_file_content(path).strip()
320 if self._remote.in_store(file_id):
320 if self._remote.in_store(file_id):
321 path = self._remote.store_path(file_id)
321 path = self._remote.store_path(file_id)
322 return LargeFileNode(path, commit=self)
322 return LargeFileNode(path, commit=self)
323 elif self._remote.in_user_cache(file_id):
323 elif self._remote.in_user_cache(file_id):
324 path = self._remote.store_path(file_id)
324 path = self._remote.store_path(file_id)
325 self._remote.link(file_id, path)
325 self._remote.link(file_id, path)
326 return LargeFileNode(path, commit=self)
326 return LargeFileNode(path, commit=self)
327
327
328 @LazyProperty
328 @LazyProperty
329 def _submodules(self):
329 def _submodules(self):
330 """
330 """
331 Returns a dictionary with submodule information from substate file
331 Returns a dictionary with submodule information from substate file
332 of hg repository.
332 of hg repository.
333 """
333 """
334 return self._remote.ctx_substate(self.idx)
334 return self._remote.ctx_substate(self.idx)
335
335
336 @LazyProperty
336 @LazyProperty
337 def affected_files(self):
337 def affected_files(self):
338 """
338 """
339 Gets a fast accessible file changes for given commit
339 Gets a fast accessible file changes for given commit
340 """
340 """
341 return self._remote.ctx_files(self.idx)
341 return self._remote.ctx_files(self.idx)
342
342
343 @property
343 @property
344 def added(self):
344 def added(self):
345 """
345 """
346 Returns list of added ``FileNode`` objects.
346 Returns list of added ``FileNode`` objects.
347 """
347 """
348 return AddedFileNodesGenerator([n for n in self.status[1]], self)
348 return AddedFileNodesGenerator([n for n in self.status[1]], self)
349
349
350 @property
350 @property
351 def changed(self):
351 def changed(self):
352 """
352 """
353 Returns list of modified ``FileNode`` objects.
353 Returns list of modified ``FileNode`` objects.
354 """
354 """
355 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
355 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
356
356
357 @property
357 @property
358 def removed(self):
358 def removed(self):
359 """
359 """
360 Returns list of removed ``FileNode`` objects.
360 Returns list of removed ``FileNode`` objects.
361 """
361 """
362 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
362 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,236 +1,236 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SVN commit module
22 SVN commit module
23 """
23 """
24
24
25
25
26 import dateutil.parser
26 import dateutil.parser
27 from zope.cachedescriptors.property import Lazy as LazyProperty
27 from zope.cachedescriptors.property import Lazy as LazyProperty
28
28
29 from rhodecode.lib.utils import safe_str, safe_unicode
29 from rhodecode.lib.utils import safe_str, safe_unicode
30 from rhodecode.lib.vcs import nodes, path as vcspath
30 from rhodecode.lib.vcs import nodes, path as vcspath
31 from rhodecode.lib.vcs.backends import base
31 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
32 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
33
33
34
34
35 _SVN_PROP_TRUE = '*'
35 _SVN_PROP_TRUE = '*'
36
36
37
37
38 class SubversionCommit(base.BaseCommit):
38 class SubversionCommit(base.BaseCommit):
39 """
39 """
40 Subversion specific implementation of commits
40 Subversion specific implementation of commits
41
41
42 .. attribute:: branch
42 .. attribute:: branch
43
43
44 The Subversion backend does not support to assign branches to
44 The Subversion backend does not support to assign branches to
45 specific commits. This attribute has always the value `None`.
45 specific commits. This attribute has always the value `None`.
46
46
47 """
47 """
48
48
49 def __init__(self, repository, commit_id):
49 def __init__(self, repository, commit_id):
50 self.repository = repository
50 self.repository = repository
51 self.idx = self.repository._get_commit_idx(commit_id)
51 self.idx = self.repository._get_commit_idx(commit_id)
52 self._svn_rev = self.idx + 1
52 self._svn_rev = self.idx + 1
53 self._remote = repository._remote
53 self._remote = repository._remote
54 # TODO: handling of raw_id should be a method on repository itself,
54 # TODO: handling of raw_id should be a method on repository itself,
55 # which knows how to translate commit index and commit id
55 # which knows how to translate commit index and commit id
56 self.raw_id = commit_id
56 self.raw_id = commit_id
57 self.short_id = commit_id
57 self.short_id = commit_id
58 self.id = 'r%s' % (commit_id, )
58 self.id = 'r%s' % (commit_id, )
59
59
60 # TODO: Implement the following placeholder attributes
60 # TODO: Implement the following placeholder attributes
61 self.nodes = {}
61 self.nodes = {}
62 self.tags = []
62 self.tags = []
63
63
64 @property
64 @property
65 def author(self):
65 def author(self):
66 return safe_unicode(self._properties.get('svn:author'))
66 return safe_unicode(self._properties.get('svn:author'))
67
67
68 @property
68 @property
69 def date(self):
69 def date(self):
70 return _date_from_svn_properties(self._properties)
70 return _date_from_svn_properties(self._properties)
71
71
72 @property
72 @property
73 def message(self):
73 def message(self):
74 return safe_unicode(self._properties.get('svn:log'))
74 return safe_unicode(self._properties.get('svn:log'))
75
75
76 @LazyProperty
76 @LazyProperty
77 def _properties(self):
77 def _properties(self):
78 return self._remote.revision_properties(self._svn_rev)
78 return self._remote.revision_properties(self._svn_rev)
79
79
80 @LazyProperty
80 @LazyProperty
81 def parents(self):
81 def parents(self):
82 parent_idx = self.idx - 1
82 parent_idx = self.idx - 1
83 if parent_idx >= 0:
83 if parent_idx >= 0:
84 parent = self.repository.get_commit(commit_idx=parent_idx)
84 parent = self.repository.get_commit(commit_idx=parent_idx)
85 return [parent]
85 return [parent]
86 return []
86 return []
87
87
88 @LazyProperty
88 @LazyProperty
89 def children(self):
89 def children(self):
90 child_idx = self.idx + 1
90 child_idx = self.idx + 1
91 if child_idx < len(self.repository.commit_ids):
91 if child_idx < len(self.repository.commit_ids):
92 child = self.repository.get_commit(commit_idx=child_idx)
92 child = self.repository.get_commit(commit_idx=child_idx)
93 return [child]
93 return [child]
94 return []
94 return []
95
95
96 def get_file_mode(self, path):
96 def get_file_mode(self, path):
97 # Note: Subversion flags files which are executable with a special
97 # Note: Subversion flags files which are executable with a special
98 # property `svn:executable` which is set to the value ``"*"``.
98 # property `svn:executable` which is set to the value ``"*"``.
99 if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE:
99 if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE:
100 return base.FILEMODE_EXECUTABLE
100 return base.FILEMODE_EXECUTABLE
101 else:
101 else:
102 return base.FILEMODE_DEFAULT
102 return base.FILEMODE_DEFAULT
103
103
104 def is_link(self, path):
104 def is_link(self, path):
105 # Note: Subversion has a flag for special files, the content of the
105 # Note: Subversion has a flag for special files, the content of the
106 # file contains the type of that file.
106 # file contains the type of that file.
107 if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE:
107 if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE:
108 return self.get_file_content(path).startswith('link')
108 return self.get_file_content(path).startswith('link')
109 return False
109 return False
110
110
111 def _get_file_property(self, path, name):
111 def _get_file_property(self, path, name):
112 file_properties = self._remote.node_properties(
112 file_properties = self._remote.node_properties(
113 safe_str(path), self._svn_rev)
113 safe_str(path), self._svn_rev)
114 return file_properties.get(name)
114 return file_properties.get(name)
115
115
116 def get_file_content(self, path):
116 def get_file_content(self, path):
117 path = self._fix_path(path)
117 path = self._fix_path(path)
118 return self._remote.get_file_content(safe_str(path), self._svn_rev)
118 return self._remote.get_file_content(safe_str(path), self._svn_rev)
119
119
120 def get_file_size(self, path):
120 def get_file_size(self, path):
121 path = self._fix_path(path)
121 path = self._fix_path(path)
122 return self._remote.get_file_size(safe_str(path), self._svn_rev)
122 return self._remote.get_file_size(safe_str(path), self._svn_rev)
123
123
124 def get_file_history(self, path, limit=None, pre_load=None):
124 def get_file_history(self, path, limit=None, pre_load=None):
125 path = safe_str(self._fix_path(path))
125 path = safe_str(self._fix_path(path))
126 history = self._remote.node_history(path, self._svn_rev, limit)
126 history = self._remote.node_history(path, self._svn_rev, limit)
127 return [
127 return [
128 self.repository.get_commit(commit_id=str(svn_rev))
128 self.repository.get_commit(commit_id=str(svn_rev))
129 for svn_rev in history]
129 for svn_rev in history]
130
130
131 def get_file_annotate(self, path, pre_load=None):
131 def get_file_annotate(self, path, pre_load=None):
132 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
132 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
133
133
134 for zero_based_line_no, svn_rev, content in result:
134 for zero_based_line_no, svn_rev, content in result:
135 commit_id = str(svn_rev)
135 commit_id = str(svn_rev)
136 line_no = zero_based_line_no + 1
136 line_no = zero_based_line_no + 1
137 yield (
137 yield (
138 line_no,
138 line_no,
139 commit_id,
139 commit_id,
140 lambda: self.repository.get_commit(commit_id=commit_id),
140 lambda: self.repository.get_commit(commit_id=commit_id),
141 content)
141 content)
142
142
143 def get_node(self, path):
143 def get_node(self, path, pre_load=None):
144 path = self._fix_path(path)
144 path = self._fix_path(path)
145 if path not in self.nodes:
145 if path not in self.nodes:
146
146
147 if path == '':
147 if path == '':
148 node = nodes.RootNode(commit=self)
148 node = nodes.RootNode(commit=self)
149 else:
149 else:
150 node_type = self._remote.get_node_type(
150 node_type = self._remote.get_node_type(
151 safe_str(path), self._svn_rev)
151 safe_str(path), self._svn_rev)
152 if node_type == 'dir':
152 if node_type == 'dir':
153 node = nodes.DirNode(path, commit=self)
153 node = nodes.DirNode(path, commit=self)
154 elif node_type == 'file':
154 elif node_type == 'file':
155 node = nodes.FileNode(path, commit=self)
155 node = nodes.FileNode(path, commit=self, pre_load=pre_load)
156 else:
156 else:
157 raise NodeDoesNotExistError(self.no_node_at_path(path))
157 raise NodeDoesNotExistError(self.no_node_at_path(path))
158
158
159 self.nodes[path] = node
159 self.nodes[path] = node
160 return self.nodes[path]
160 return self.nodes[path]
161
161
162 def get_nodes(self, path):
162 def get_nodes(self, path):
163 if self._get_kind(path) != nodes.NodeKind.DIR:
163 if self._get_kind(path) != nodes.NodeKind.DIR:
164 raise CommitError(
164 raise CommitError(
165 "Directory does not exist for commit %s at "
165 "Directory does not exist for commit %s at "
166 " '%s'" % (self.raw_id, path))
166 " '%s'" % (self.raw_id, path))
167 path = self._fix_path(path)
167 path = self._fix_path(path)
168
168
169 path_nodes = []
169 path_nodes = []
170 for name, kind in self._remote.get_nodes(
170 for name, kind in self._remote.get_nodes(
171 safe_str(path), revision=self._svn_rev):
171 safe_str(path), revision=self._svn_rev):
172 node_path = vcspath.join(path, name)
172 node_path = vcspath.join(path, name)
173 if kind == 'dir':
173 if kind == 'dir':
174 node = nodes.DirNode(node_path, commit=self)
174 node = nodes.DirNode(node_path, commit=self)
175 elif kind == 'file':
175 elif kind == 'file':
176 node = nodes.FileNode(node_path, commit=self)
176 node = nodes.FileNode(node_path, commit=self)
177 else:
177 else:
178 raise ValueError("Node kind %s not supported." % (kind, ))
178 raise ValueError("Node kind %s not supported." % (kind, ))
179 self.nodes[node_path] = node
179 self.nodes[node_path] = node
180 path_nodes.append(node)
180 path_nodes.append(node)
181
181
182 return path_nodes
182 return path_nodes
183
183
184 def _get_kind(self, path):
184 def _get_kind(self, path):
185 path = self._fix_path(path)
185 path = self._fix_path(path)
186 kind = self._remote.get_node_type(path, self._svn_rev)
186 kind = self._remote.get_node_type(path, self._svn_rev)
187 if kind == 'file':
187 if kind == 'file':
188 return nodes.NodeKind.FILE
188 return nodes.NodeKind.FILE
189 elif kind == 'dir':
189 elif kind == 'dir':
190 return nodes.NodeKind.DIR
190 return nodes.NodeKind.DIR
191 else:
191 else:
192 raise CommitError(
192 raise CommitError(
193 "Node does not exist at the given path '%s'" % (path, ))
193 "Node does not exist at the given path '%s'" % (path, ))
194
194
195 @LazyProperty
195 @LazyProperty
196 def _changes_cache(self):
196 def _changes_cache(self):
197 return self._remote.revision_changes(self._svn_rev)
197 return self._remote.revision_changes(self._svn_rev)
198
198
199 @LazyProperty
199 @LazyProperty
200 def affected_files(self):
200 def affected_files(self):
201 changed_files = set()
201 changed_files = set()
202 for files in self._changes_cache.itervalues():
202 for files in self._changes_cache.itervalues():
203 changed_files.update(files)
203 changed_files.update(files)
204 return list(changed_files)
204 return list(changed_files)
205
205
206 @LazyProperty
206 @LazyProperty
207 def id(self):
207 def id(self):
208 return self.raw_id
208 return self.raw_id
209
209
210 @property
210 @property
211 def added(self):
211 def added(self):
212 return nodes.AddedFileNodesGenerator(
212 return nodes.AddedFileNodesGenerator(
213 self._changes_cache['added'], self)
213 self._changes_cache['added'], self)
214
214
215 @property
215 @property
216 def changed(self):
216 def changed(self):
217 return nodes.ChangedFileNodesGenerator(
217 return nodes.ChangedFileNodesGenerator(
218 self._changes_cache['changed'], self)
218 self._changes_cache['changed'], self)
219
219
220 @property
220 @property
221 def removed(self):
221 def removed(self):
222 return nodes.RemovedFileNodesGenerator(
222 return nodes.RemovedFileNodesGenerator(
223 self._changes_cache['removed'], self)
223 self._changes_cache['removed'], self)
224
224
225
225
226 def _date_from_svn_properties(properties):
226 def _date_from_svn_properties(properties):
227 """
227 """
228 Parses the date out of given svn properties.
228 Parses the date out of given svn properties.
229
229
230 :return: :class:`datetime.datetime` instance. The object is naive.
230 :return: :class:`datetime.datetime` instance. The object is naive.
231 """
231 """
232
232
233 aware_date = dateutil.parser.parse(properties.get('svn:date'))
233 aware_date = dateutil.parser.parse(properties.get('svn:date'))
234 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
234 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
235 final_date = aware_date
235 final_date = aware_date
236 return final_date.replace(tzinfo=None)
236 return final_date.replace(tzinfo=None)
@@ -1,756 +1,773 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Module holding everything related to vcs nodes, with vcs2 architecture.
22 Module holding everything related to vcs nodes, with vcs2 architecture.
23 """
23 """
24
24
25
25
26 import stat
26 import stat
27
27
28 from zope.cachedescriptors.property import Lazy as LazyProperty
28 from zope.cachedescriptors.property import Lazy as LazyProperty
29
29
30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
31 from rhodecode.lib.utils import safe_unicode, safe_str
31 from rhodecode.lib.utils import safe_unicode, safe_str
32 from rhodecode.lib.utils2 import md5
32 from rhodecode.lib.utils2 import md5
33 from rhodecode.lib.vcs import path as vcspath
33 from rhodecode.lib.vcs import path as vcspath
34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
37
37
38 LARGEFILE_PREFIX = '.hglf'
38 LARGEFILE_PREFIX = '.hglf'
39
39
40
40
41 class NodeKind:
41 class NodeKind:
42 SUBMODULE = -1
42 SUBMODULE = -1
43 DIR = 1
43 DIR = 1
44 FILE = 2
44 FILE = 2
45 LARGEFILE = 3
45 LARGEFILE = 3
46
46
47
47
48 class NodeState:
48 class NodeState:
49 ADDED = u'added'
49 ADDED = u'added'
50 CHANGED = u'changed'
50 CHANGED = u'changed'
51 NOT_CHANGED = u'not changed'
51 NOT_CHANGED = u'not changed'
52 REMOVED = u'removed'
52 REMOVED = u'removed'
53
53
54
54
55 class NodeGeneratorBase(object):
55 class NodeGeneratorBase(object):
56 """
56 """
57 Base class for removed added and changed filenodes, it's a lazy generator
57 Base class for removed added and changed filenodes, it's a lazy generator
58 class that will create filenodes only on iteration or call
58 class that will create filenodes only on iteration or call
59
59
60 The len method doesn't need to create filenodes at all
60 The len method doesn't need to create filenodes at all
61 """
61 """
62
62
63 def __init__(self, current_paths, cs):
63 def __init__(self, current_paths, cs):
64 self.cs = cs
64 self.cs = cs
65 self.current_paths = current_paths
65 self.current_paths = current_paths
66
66
67 def __call__(self):
67 def __call__(self):
68 return [n for n in self]
68 return [n for n in self]
69
69
70 def __getslice__(self, i, j):
70 def __getslice__(self, i, j):
71 for p in self.current_paths[i:j]:
71 for p in self.current_paths[i:j]:
72 yield self.cs.get_node(p)
72 yield self.cs.get_node(p)
73
73
74 def __len__(self):
74 def __len__(self):
75 return len(self.current_paths)
75 return len(self.current_paths)
76
76
77 def __iter__(self):
77 def __iter__(self):
78 for p in self.current_paths:
78 for p in self.current_paths:
79 yield self.cs.get_node(p)
79 yield self.cs.get_node(p)
80
80
81
81
82 class AddedFileNodesGenerator(NodeGeneratorBase):
82 class AddedFileNodesGenerator(NodeGeneratorBase):
83 """
83 """
84 Class holding added files for current commit
84 Class holding added files for current commit
85 """
85 """
86
86
87
87
88 class ChangedFileNodesGenerator(NodeGeneratorBase):
88 class ChangedFileNodesGenerator(NodeGeneratorBase):
89 """
89 """
90 Class holding changed files for current commit
90 Class holding changed files for current commit
91 """
91 """
92
92
93
93
94 class RemovedFileNodesGenerator(NodeGeneratorBase):
94 class RemovedFileNodesGenerator(NodeGeneratorBase):
95 """
95 """
96 Class holding removed files for current commit
96 Class holding removed files for current commit
97 """
97 """
98 def __iter__(self):
98 def __iter__(self):
99 for p in self.current_paths:
99 for p in self.current_paths:
100 yield RemovedFileNode(path=p)
100 yield RemovedFileNode(path=p)
101
101
102 def __getslice__(self, i, j):
102 def __getslice__(self, i, j):
103 for p in self.current_paths[i:j]:
103 for p in self.current_paths[i:j]:
104 yield RemovedFileNode(path=p)
104 yield RemovedFileNode(path=p)
105
105
106
106
107 class Node(object):
107 class Node(object):
108 """
108 """
109 Simplest class representing file or directory on repository. SCM backends
109 Simplest class representing file or directory on repository. SCM backends
110 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
110 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
111 directly.
111 directly.
112
112
113 Node's ``path`` cannot start with slash as we operate on *relative* paths
113 Node's ``path`` cannot start with slash as we operate on *relative* paths
114 only. Moreover, every single node is identified by the ``path`` attribute,
114 only. Moreover, every single node is identified by the ``path`` attribute,
115 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
115 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
116 """
116 """
117
117
118 commit = None
118 commit = None
119
119
120 def __init__(self, path, kind):
120 def __init__(self, path, kind):
121 self._validate_path(path) # can throw exception if path is invalid
121 self._validate_path(path) # can throw exception if path is invalid
122 self.path = safe_str(path.rstrip('/')) # we store paths as str
122 self.path = safe_str(path.rstrip('/')) # we store paths as str
123 if path == '' and kind != NodeKind.DIR:
123 if path == '' and kind != NodeKind.DIR:
124 raise NodeError("Only DirNode and its subclasses may be "
124 raise NodeError("Only DirNode and its subclasses may be "
125 "initialized with empty path")
125 "initialized with empty path")
126 self.kind = kind
126 self.kind = kind
127
127
128 if self.is_root() and not self.is_dir():
128 if self.is_root() and not self.is_dir():
129 raise NodeError("Root node cannot be FILE kind")
129 raise NodeError("Root node cannot be FILE kind")
130
130
131 def _validate_path(self, path):
131 def _validate_path(self, path):
132 if path.startswith('/'):
132 if path.startswith('/'):
133 raise NodeError(
133 raise NodeError(
134 "Cannot initialize Node objects with slash at "
134 "Cannot initialize Node objects with slash at "
135 "the beginning as only relative paths are supported. "
135 "the beginning as only relative paths are supported. "
136 "Got %s" % (path,))
136 "Got %s" % (path,))
137
137
138 @LazyProperty
138 @LazyProperty
139 def parent(self):
139 def parent(self):
140 parent_path = self.get_parent_path()
140 parent_path = self.get_parent_path()
141 if parent_path:
141 if parent_path:
142 if self.commit:
142 if self.commit:
143 return self.commit.get_node(parent_path)
143 return self.commit.get_node(parent_path)
144 return DirNode(parent_path)
144 return DirNode(parent_path)
145 return None
145 return None
146
146
147 @LazyProperty
147 @LazyProperty
148 def unicode_path(self):
148 def unicode_path(self):
149 return safe_unicode(self.path)
149 return safe_unicode(self.path)
150
150
151 @LazyProperty
151 @LazyProperty
152 def dir_path(self):
152 def dir_path(self):
153 """
153 """
154 Returns name of the directory from full path of this vcs node. Empty
154 Returns name of the directory from full path of this vcs node. Empty
155 string is returned if there's no directory in the path
155 string is returned if there's no directory in the path
156 """
156 """
157 _parts = self.path.rstrip('/').rsplit('/', 1)
157 _parts = self.path.rstrip('/').rsplit('/', 1)
158 if len(_parts) == 2:
158 if len(_parts) == 2:
159 return safe_unicode(_parts[0])
159 return safe_unicode(_parts[0])
160 return u''
160 return u''
161
161
162 @LazyProperty
162 @LazyProperty
163 def name(self):
163 def name(self):
164 """
164 """
165 Returns name of the node so if its path
165 Returns name of the node so if its path
166 then only last part is returned.
166 then only last part is returned.
167 """
167 """
168 return safe_unicode(self.path.rstrip('/').split('/')[-1])
168 return safe_unicode(self.path.rstrip('/').split('/')[-1])
169
169
170 @property
170 @property
171 def kind(self):
171 def kind(self):
172 return self._kind
172 return self._kind
173
173
174 @kind.setter
174 @kind.setter
175 def kind(self, kind):
175 def kind(self, kind):
176 if hasattr(self, '_kind'):
176 if hasattr(self, '_kind'):
177 raise NodeError("Cannot change node's kind")
177 raise NodeError("Cannot change node's kind")
178 else:
178 else:
179 self._kind = kind
179 self._kind = kind
180 # Post setter check (path's trailing slash)
180 # Post setter check (path's trailing slash)
181 if self.path.endswith('/'):
181 if self.path.endswith('/'):
182 raise NodeError("Node's path cannot end with slash")
182 raise NodeError("Node's path cannot end with slash")
183
183
184 def __cmp__(self, other):
184 def __cmp__(self, other):
185 """
185 """
186 Comparator using name of the node, needed for quick list sorting.
186 Comparator using name of the node, needed for quick list sorting.
187 """
187 """
188 kind_cmp = cmp(self.kind, other.kind)
188 kind_cmp = cmp(self.kind, other.kind)
189 if kind_cmp:
189 if kind_cmp:
190 return kind_cmp
190 return kind_cmp
191 return cmp(self.name, other.name)
191 return cmp(self.name, other.name)
192
192
193 def __eq__(self, other):
193 def __eq__(self, other):
194 for attr in ['name', 'path', 'kind']:
194 for attr in ['name', 'path', 'kind']:
195 if getattr(self, attr) != getattr(other, attr):
195 if getattr(self, attr) != getattr(other, attr):
196 return False
196 return False
197 if self.is_file():
197 if self.is_file():
198 if self.content != other.content:
198 if self.content != other.content:
199 return False
199 return False
200 else:
200 else:
201 # For DirNode's check without entering each dir
201 # For DirNode's check without entering each dir
202 self_nodes_paths = list(sorted(n.path for n in self.nodes))
202 self_nodes_paths = list(sorted(n.path for n in self.nodes))
203 other_nodes_paths = list(sorted(n.path for n in self.nodes))
203 other_nodes_paths = list(sorted(n.path for n in self.nodes))
204 if self_nodes_paths != other_nodes_paths:
204 if self_nodes_paths != other_nodes_paths:
205 return False
205 return False
206 return True
206 return True
207
207
208 def __ne__(self, other):
208 def __ne__(self, other):
209 return not self.__eq__(other)
209 return not self.__eq__(other)
210
210
211 def __repr__(self):
211 def __repr__(self):
212 return '<%s %r>' % (self.__class__.__name__, self.path)
212 return '<%s %r>' % (self.__class__.__name__, self.path)
213
213
214 def __str__(self):
214 def __str__(self):
215 return self.__repr__()
215 return self.__repr__()
216
216
217 def __unicode__(self):
217 def __unicode__(self):
218 return self.name
218 return self.name
219
219
220 def get_parent_path(self):
220 def get_parent_path(self):
221 """
221 """
222 Returns node's parent path or empty string if node is root.
222 Returns node's parent path or empty string if node is root.
223 """
223 """
224 if self.is_root():
224 if self.is_root():
225 return ''
225 return ''
226 return vcspath.dirname(self.path.rstrip('/')) + '/'
226 return vcspath.dirname(self.path.rstrip('/')) + '/'
227
227
228 def is_file(self):
228 def is_file(self):
229 """
229 """
230 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
230 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
231 otherwise.
231 otherwise.
232 """
232 """
233 return self.kind == NodeKind.FILE
233 return self.kind == NodeKind.FILE
234
234
235 def is_dir(self):
235 def is_dir(self):
236 """
236 """
237 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
237 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
238 otherwise.
238 otherwise.
239 """
239 """
240 return self.kind == NodeKind.DIR
240 return self.kind == NodeKind.DIR
241
241
242 def is_root(self):
242 def is_root(self):
243 """
243 """
244 Returns ``True`` if node is a root node and ``False`` otherwise.
244 Returns ``True`` if node is a root node and ``False`` otherwise.
245 """
245 """
246 return self.kind == NodeKind.DIR and self.path == ''
246 return self.kind == NodeKind.DIR and self.path == ''
247
247
248 def is_submodule(self):
248 def is_submodule(self):
249 """
249 """
250 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
250 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
251 otherwise.
251 otherwise.
252 """
252 """
253 return self.kind == NodeKind.SUBMODULE
253 return self.kind == NodeKind.SUBMODULE
254
254
255 def is_largefile(self):
255 def is_largefile(self):
256 """
256 """
257 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
257 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
258 otherwise
258 otherwise
259 """
259 """
260 return self.kind == NodeKind.LARGEFILE
260 return self.kind == NodeKind.LARGEFILE
261
261
262 def is_link(self):
262 def is_link(self):
263 if self.commit:
263 if self.commit:
264 return self.commit.is_link(self.path)
264 return self.commit.is_link(self.path)
265 return False
265 return False
266
266
267 @LazyProperty
267 @LazyProperty
268 def added(self):
268 def added(self):
269 return self.state is NodeState.ADDED
269 return self.state is NodeState.ADDED
270
270
271 @LazyProperty
271 @LazyProperty
272 def changed(self):
272 def changed(self):
273 return self.state is NodeState.CHANGED
273 return self.state is NodeState.CHANGED
274
274
275 @LazyProperty
275 @LazyProperty
276 def not_changed(self):
276 def not_changed(self):
277 return self.state is NodeState.NOT_CHANGED
277 return self.state is NodeState.NOT_CHANGED
278
278
279 @LazyProperty
279 @LazyProperty
280 def removed(self):
280 def removed(self):
281 return self.state is NodeState.REMOVED
281 return self.state is NodeState.REMOVED
282
282
283
283
284 class FileNode(Node):
284 class FileNode(Node):
285 """
285 """
286 Class representing file nodes.
286 Class representing file nodes.
287
287
288 :attribute: path: path to the node, relative to repository's root
288 :attribute: path: path to the node, relative to repository's root
289 :attribute: content: if given arbitrary sets content of the file
289 :attribute: content: if given arbitrary sets content of the file
290 :attribute: commit: if given, first time content is accessed, callback
290 :attribute: commit: if given, first time content is accessed, callback
291 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
291 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
292 """
292 """
293 _filter_pre_load = []
293
294
294 def __init__(self, path, content=None, commit=None, mode=None):
295 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
295 """
296 """
296 Only one of ``content`` and ``commit`` may be given. Passing both
297 Only one of ``content`` and ``commit`` may be given. Passing both
297 would raise ``NodeError`` exception.
298 would raise ``NodeError`` exception.
298
299
299 :param path: relative path to the node
300 :param path: relative path to the node
300 :param content: content may be passed to constructor
301 :param content: content may be passed to constructor
301 :param commit: if given, will use it to lazily fetch content
302 :param commit: if given, will use it to lazily fetch content
302 :param mode: ST_MODE (i.e. 0100644)
303 :param mode: ST_MODE (i.e. 0100644)
303 """
304 """
304 if content and commit:
305 if content and commit:
305 raise NodeError("Cannot use both content and commit")
306 raise NodeError("Cannot use both content and commit")
306 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
307 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
307 self.commit = commit
308 self.commit = commit
308 self._content = content
309 self._content = content
309 self._mode = mode or FILEMODE_DEFAULT
310 self._mode = mode or FILEMODE_DEFAULT
310
311
312 self._set_bulk_properties(pre_load)
313
314 def _set_bulk_properties(self, pre_load):
315 if not pre_load:
316 return
317 pre_load = [entry for entry in pre_load
318 if entry not in self._filter_pre_load]
319 if not pre_load:
320 return
321
322 for attr_name in pre_load:
323 result = getattr(self, attr_name)
324 if callable(result):
325 result = result()
326 self.__dict__[attr_name] = result
327
311 @LazyProperty
328 @LazyProperty
312 def mode(self):
329 def mode(self):
313 """
330 """
314 Returns lazily mode of the FileNode. If `commit` is not set, would
331 Returns lazily mode of the FileNode. If `commit` is not set, would
315 use value given at initialization or `FILEMODE_DEFAULT` (default).
332 use value given at initialization or `FILEMODE_DEFAULT` (default).
316 """
333 """
317 if self.commit:
334 if self.commit:
318 mode = self.commit.get_file_mode(self.path)
335 mode = self.commit.get_file_mode(self.path)
319 else:
336 else:
320 mode = self._mode
337 mode = self._mode
321 return mode
338 return mode
322
339
323 @LazyProperty
340 @LazyProperty
324 def raw_bytes(self):
341 def raw_bytes(self):
325 """
342 """
326 Returns lazily the raw bytes of the FileNode.
343 Returns lazily the raw bytes of the FileNode.
327 """
344 """
328 if self.commit:
345 if self.commit:
329 if self._content is None:
346 if self._content is None:
330 self._content = self.commit.get_file_content(self.path)
347 self._content = self.commit.get_file_content(self.path)
331 content = self._content
348 content = self._content
332 else:
349 else:
333 content = self._content
350 content = self._content
334 return content
351 return content
335
352
336 @LazyProperty
353 @LazyProperty
337 def md5(self):
354 def md5(self):
338 """
355 """
339 Returns md5 of the file node.
356 Returns md5 of the file node.
340 """
357 """
341 return md5(self.raw_bytes)
358 return md5(self.raw_bytes)
342
359
343 @LazyProperty
360 @LazyProperty
344 def content(self):
361 def content(self):
345 """
362 """
346 Returns lazily content of the FileNode. If possible, would try to
363 Returns lazily content of the FileNode. If possible, would try to
347 decode content from UTF-8.
364 decode content from UTF-8.
348 """
365 """
349 content = self.raw_bytes
366 content = self.raw_bytes
350
367
351 if self.is_binary:
368 if self.is_binary:
352 return content
369 return content
353 return safe_unicode(content)
370 return safe_unicode(content)
354
371
355 @LazyProperty
372 @LazyProperty
356 def size(self):
373 def size(self):
357 if self.commit:
374 if self.commit:
358 return self.commit.get_file_size(self.path)
375 return self.commit.get_file_size(self.path)
359 raise NodeError(
376 raise NodeError(
360 "Cannot retrieve size of the file without related "
377 "Cannot retrieve size of the file without related "
361 "commit attribute")
378 "commit attribute")
362
379
363 @LazyProperty
380 @LazyProperty
364 def message(self):
381 def message(self):
365 if self.commit:
382 if self.commit:
366 return self.last_commit.message
383 return self.last_commit.message
367 raise NodeError(
384 raise NodeError(
368 "Cannot retrieve message of the file without related "
385 "Cannot retrieve message of the file without related "
369 "commit attribute")
386 "commit attribute")
370
387
371 @LazyProperty
388 @LazyProperty
372 def last_commit(self):
389 def last_commit(self):
373 if self.commit:
390 if self.commit:
374 pre_load = ["author", "date", "message"]
391 pre_load = ["author", "date", "message"]
375 return self.commit.get_file_commit(self.path, pre_load=pre_load)
392 return self.commit.get_file_commit(self.path, pre_load=pre_load)
376 raise NodeError(
393 raise NodeError(
377 "Cannot retrieve last commit of the file without "
394 "Cannot retrieve last commit of the file without "
378 "related commit attribute")
395 "related commit attribute")
379
396
380 def get_mimetype(self):
397 def get_mimetype(self):
381 """
398 """
382 Mimetype is calculated based on the file's content. If ``_mimetype``
399 Mimetype is calculated based on the file's content. If ``_mimetype``
383 attribute is available, it will be returned (backends which store
400 attribute is available, it will be returned (backends which store
384 mimetypes or can easily recognize them, should set this private
401 mimetypes or can easily recognize them, should set this private
385 attribute to indicate that type should *NOT* be calculated).
402 attribute to indicate that type should *NOT* be calculated).
386 """
403 """
387
404
388 if hasattr(self, '_mimetype'):
405 if hasattr(self, '_mimetype'):
389 if (isinstance(self._mimetype, (tuple, list,)) and
406 if (isinstance(self._mimetype, (tuple, list,)) and
390 len(self._mimetype) == 2):
407 len(self._mimetype) == 2):
391 return self._mimetype
408 return self._mimetype
392 else:
409 else:
393 raise NodeError('given _mimetype attribute must be an 2 '
410 raise NodeError('given _mimetype attribute must be an 2 '
394 'element list or tuple')
411 'element list or tuple')
395
412
396 db = get_mimetypes_db()
413 db = get_mimetypes_db()
397 mtype, encoding = db.guess_type(self.name)
414 mtype, encoding = db.guess_type(self.name)
398
415
399 if mtype is None:
416 if mtype is None:
400 if self.is_binary:
417 if self.is_binary:
401 mtype = 'application/octet-stream'
418 mtype = 'application/octet-stream'
402 encoding = None
419 encoding = None
403 else:
420 else:
404 mtype = 'text/plain'
421 mtype = 'text/plain'
405 encoding = None
422 encoding = None
406
423
407 # try with pygments
424 # try with pygments
408 try:
425 try:
409 from pygments.lexers import get_lexer_for_filename
426 from pygments.lexers import get_lexer_for_filename
410 mt = get_lexer_for_filename(self.name).mimetypes
427 mt = get_lexer_for_filename(self.name).mimetypes
411 except Exception:
428 except Exception:
412 mt = None
429 mt = None
413
430
414 if mt:
431 if mt:
415 mtype = mt[0]
432 mtype = mt[0]
416
433
417 return mtype, encoding
434 return mtype, encoding
418
435
419 @LazyProperty
436 @LazyProperty
420 def mimetype(self):
437 def mimetype(self):
421 """
438 """
422 Wrapper around full mimetype info. It returns only type of fetched
439 Wrapper around full mimetype info. It returns only type of fetched
423 mimetype without the encoding part. use get_mimetype function to fetch
440 mimetype without the encoding part. use get_mimetype function to fetch
424 full set of (type,encoding)
441 full set of (type,encoding)
425 """
442 """
426 return self.get_mimetype()[0]
443 return self.get_mimetype()[0]
427
444
428 @LazyProperty
445 @LazyProperty
429 def mimetype_main(self):
446 def mimetype_main(self):
430 return self.mimetype.split('/')[0]
447 return self.mimetype.split('/')[0]
431
448
432 @LazyProperty
449 @LazyProperty
433 def lexer(self):
450 def lexer(self):
434 """
451 """
435 Returns pygment's lexer class. Would try to guess lexer taking file's
452 Returns pygment's lexer class. Would try to guess lexer taking file's
436 content, name and mimetype.
453 content, name and mimetype.
437 """
454 """
438 from pygments import lexers
455 from pygments import lexers
439
456
440 lexer = None
457 lexer = None
441 try:
458 try:
442 lexer = lexers.guess_lexer_for_filename(
459 lexer = lexers.guess_lexer_for_filename(
443 self.name, self.content, stripnl=False)
460 self.name, self.content, stripnl=False)
444 except lexers.ClassNotFound:
461 except lexers.ClassNotFound:
445 lexer = None
462 lexer = None
446
463
447 # try our EXTENSION_MAP
464 # try our EXTENSION_MAP
448 if not lexer:
465 if not lexer:
449 try:
466 try:
450 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(self.extension)
467 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(self.extension)
451 if lexer_class:
468 if lexer_class:
452 lexer = lexers.get_lexer_by_name(lexer_class[0])
469 lexer = lexers.get_lexer_by_name(lexer_class[0])
453 except lexers.ClassNotFound:
470 except lexers.ClassNotFound:
454 lexer = None
471 lexer = None
455
472
456 if not lexer:
473 if not lexer:
457 lexer = lexers.TextLexer(stripnl=False)
474 lexer = lexers.TextLexer(stripnl=False)
458
475
459 return lexer
476 return lexer
460
477
461 @LazyProperty
478 @LazyProperty
462 def lexer_alias(self):
479 def lexer_alias(self):
463 """
480 """
464 Returns first alias of the lexer guessed for this file.
481 Returns first alias of the lexer guessed for this file.
465 """
482 """
466 return self.lexer.aliases[0]
483 return self.lexer.aliases[0]
467
484
468 @LazyProperty
485 @LazyProperty
469 def history(self):
486 def history(self):
470 """
487 """
471 Returns a list of commit for this file in which the file was changed
488 Returns a list of commit for this file in which the file was changed
472 """
489 """
473 if self.commit is None:
490 if self.commit is None:
474 raise NodeError('Unable to get commit for this FileNode')
491 raise NodeError('Unable to get commit for this FileNode')
475 return self.commit.get_file_history(self.path)
492 return self.commit.get_file_history(self.path)
476
493
477 @LazyProperty
494 @LazyProperty
478 def annotate(self):
495 def annotate(self):
479 """
496 """
480 Returns a list of three element tuples with lineno, commit and line
497 Returns a list of three element tuples with lineno, commit and line
481 """
498 """
482 if self.commit is None:
499 if self.commit is None:
483 raise NodeError('Unable to get commit for this FileNode')
500 raise NodeError('Unable to get commit for this FileNode')
484 pre_load = ["author", "date", "message"]
501 pre_load = ["author", "date", "message"]
485 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
502 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
486
503
487 @LazyProperty
504 @LazyProperty
488 def state(self):
505 def state(self):
489 if not self.commit:
506 if not self.commit:
490 raise NodeError(
507 raise NodeError(
491 "Cannot check state of the node if it's not "
508 "Cannot check state of the node if it's not "
492 "linked with commit")
509 "linked with commit")
493 elif self.path in (node.path for node in self.commit.added):
510 elif self.path in (node.path for node in self.commit.added):
494 return NodeState.ADDED
511 return NodeState.ADDED
495 elif self.path in (node.path for node in self.commit.changed):
512 elif self.path in (node.path for node in self.commit.changed):
496 return NodeState.CHANGED
513 return NodeState.CHANGED
497 else:
514 else:
498 return NodeState.NOT_CHANGED
515 return NodeState.NOT_CHANGED
499
516
500 @LazyProperty
517 @LazyProperty
501 def is_binary(self):
518 def is_binary(self):
502 """
519 """
503 Returns True if file has binary content.
520 Returns True if file has binary content.
504 """
521 """
505 _bin = self.raw_bytes and '\0' in self.raw_bytes
522 _bin = self.raw_bytes and '\0' in self.raw_bytes
506 return _bin
523 return _bin
507
524
508 @LazyProperty
525 @LazyProperty
509 def extension(self):
526 def extension(self):
510 """Returns filenode extension"""
527 """Returns filenode extension"""
511 return self.name.split('.')[-1]
528 return self.name.split('.')[-1]
512
529
513 @property
530 @property
514 def is_executable(self):
531 def is_executable(self):
515 """
532 """
516 Returns ``True`` if file has executable flag turned on.
533 Returns ``True`` if file has executable flag turned on.
517 """
534 """
518 return bool(self.mode & stat.S_IXUSR)
535 return bool(self.mode & stat.S_IXUSR)
519
536
520 def get_largefile_node(self):
537 def get_largefile_node(self):
521 """
538 """
522 Try to return a Mercurial FileNode from this node. It does internal
539 Try to return a Mercurial FileNode from this node. It does internal
523 checks inside largefile store, if that file exist there it will
540 checks inside largefile store, if that file exist there it will
524 create special instance of LargeFileNode which can get content from
541 create special instance of LargeFileNode which can get content from
525 LF store.
542 LF store.
526 """
543 """
527 if self.commit and self.path.startswith(LARGEFILE_PREFIX):
544 if self.commit and self.path.startswith(LARGEFILE_PREFIX):
528 largefile_path = self.path.split(LARGEFILE_PREFIX)[-1].lstrip('/')
545 largefile_path = self.path.split(LARGEFILE_PREFIX)[-1].lstrip('/')
529 return self.commit.get_largefile_node(largefile_path)
546 return self.commit.get_largefile_node(largefile_path)
530
547
531 def lines(self, count_empty=False):
548 def lines(self, count_empty=False):
532 all_lines, empty_lines = 0, 0
549 all_lines, empty_lines = 0, 0
533
550
534 if not self.is_binary:
551 if not self.is_binary:
535 content = self.content
552 content = self.content
536 if count_empty:
553 if count_empty:
537 all_lines = 0
554 all_lines = 0
538 empty_lines = 0
555 empty_lines = 0
539 for line in content.splitlines(True):
556 for line in content.splitlines(True):
540 if line == '\n':
557 if line == '\n':
541 empty_lines += 1
558 empty_lines += 1
542 all_lines += 1
559 all_lines += 1
543
560
544 return all_lines, all_lines - empty_lines
561 return all_lines, all_lines - empty_lines
545 else:
562 else:
546 # fast method
563 # fast method
547 empty_lines = all_lines = content.count('\n')
564 empty_lines = all_lines = content.count('\n')
548 if all_lines == 0 and content:
565 if all_lines == 0 and content:
549 # one-line without a newline
566 # one-line without a newline
550 empty_lines = all_lines = 1
567 empty_lines = all_lines = 1
551
568
552 return all_lines, empty_lines
569 return all_lines, empty_lines
553
570
554 def __repr__(self):
571 def __repr__(self):
555 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
572 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
556 getattr(self.commit, 'short_id', ''))
573 getattr(self.commit, 'short_id', ''))
557
574
558
575
559 class RemovedFileNode(FileNode):
576 class RemovedFileNode(FileNode):
560 """
577 """
561 Dummy FileNode class - trying to access any public attribute except path,
578 Dummy FileNode class - trying to access any public attribute except path,
562 name, kind or state (or methods/attributes checking those two) would raise
579 name, kind or state (or methods/attributes checking those two) would raise
563 RemovedFileNodeError.
580 RemovedFileNodeError.
564 """
581 """
565 ALLOWED_ATTRIBUTES = [
582 ALLOWED_ATTRIBUTES = [
566 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
583 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
567 'added', 'changed', 'not_changed', 'removed'
584 'added', 'changed', 'not_changed', 'removed'
568 ]
585 ]
569
586
570 def __init__(self, path):
587 def __init__(self, path):
571 """
588 """
572 :param path: relative path to the node
589 :param path: relative path to the node
573 """
590 """
574 super(RemovedFileNode, self).__init__(path=path)
591 super(RemovedFileNode, self).__init__(path=path)
575
592
576 def __getattribute__(self, attr):
593 def __getattribute__(self, attr):
577 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
594 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
578 return super(RemovedFileNode, self).__getattribute__(attr)
595 return super(RemovedFileNode, self).__getattribute__(attr)
579 raise RemovedFileNodeError(
596 raise RemovedFileNodeError(
580 "Cannot access attribute %s on RemovedFileNode" % attr)
597 "Cannot access attribute %s on RemovedFileNode" % attr)
581
598
582 @LazyProperty
599 @LazyProperty
583 def state(self):
600 def state(self):
584 return NodeState.REMOVED
601 return NodeState.REMOVED
585
602
586
603
587 class DirNode(Node):
604 class DirNode(Node):
588 """
605 """
589 DirNode stores list of files and directories within this node.
606 DirNode stores list of files and directories within this node.
590 Nodes may be used standalone but within repository context they
607 Nodes may be used standalone but within repository context they
591 lazily fetch data within same repositorty's commit.
608 lazily fetch data within same repositorty's commit.
592 """
609 """
593
610
594 def __init__(self, path, nodes=(), commit=None):
611 def __init__(self, path, nodes=(), commit=None):
595 """
612 """
596 Only one of ``nodes`` and ``commit`` may be given. Passing both
613 Only one of ``nodes`` and ``commit`` may be given. Passing both
597 would raise ``NodeError`` exception.
614 would raise ``NodeError`` exception.
598
615
599 :param path: relative path to the node
616 :param path: relative path to the node
600 :param nodes: content may be passed to constructor
617 :param nodes: content may be passed to constructor
601 :param commit: if given, will use it to lazily fetch content
618 :param commit: if given, will use it to lazily fetch content
602 """
619 """
603 if nodes and commit:
620 if nodes and commit:
604 raise NodeError("Cannot use both nodes and commit")
621 raise NodeError("Cannot use both nodes and commit")
605 super(DirNode, self).__init__(path, NodeKind.DIR)
622 super(DirNode, self).__init__(path, NodeKind.DIR)
606 self.commit = commit
623 self.commit = commit
607 self._nodes = nodes
624 self._nodes = nodes
608
625
609 @LazyProperty
626 @LazyProperty
610 def content(self):
627 def content(self):
611 raise NodeError(
628 raise NodeError(
612 "%s represents a dir and has no `content` attribute" % self)
629 "%s represents a dir and has no `content` attribute" % self)
613
630
614 @LazyProperty
631 @LazyProperty
615 def nodes(self):
632 def nodes(self):
616 if self.commit:
633 if self.commit:
617 nodes = self.commit.get_nodes(self.path)
634 nodes = self.commit.get_nodes(self.path)
618 else:
635 else:
619 nodes = self._nodes
636 nodes = self._nodes
620 self._nodes_dict = dict((node.path, node) for node in nodes)
637 self._nodes_dict = dict((node.path, node) for node in nodes)
621 return sorted(nodes)
638 return sorted(nodes)
622
639
623 @LazyProperty
640 @LazyProperty
624 def files(self):
641 def files(self):
625 return sorted((node for node in self.nodes if node.is_file()))
642 return sorted((node for node in self.nodes if node.is_file()))
626
643
627 @LazyProperty
644 @LazyProperty
628 def dirs(self):
645 def dirs(self):
629 return sorted((node for node in self.nodes if node.is_dir()))
646 return sorted((node for node in self.nodes if node.is_dir()))
630
647
631 def __iter__(self):
648 def __iter__(self):
632 for node in self.nodes:
649 for node in self.nodes:
633 yield node
650 yield node
634
651
635 def get_node(self, path):
652 def get_node(self, path):
636 """
653 """
637 Returns node from within this particular ``DirNode``, so it is now
654 Returns node from within this particular ``DirNode``, so it is now
638 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
655 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
639 'docs'. In order to access deeper nodes one must fetch nodes between
656 'docs'. In order to access deeper nodes one must fetch nodes between
640 them first - this would work::
657 them first - this would work::
641
658
642 docs = root.get_node('docs')
659 docs = root.get_node('docs')
643 docs.get_node('api').get_node('index.rst')
660 docs.get_node('api').get_node('index.rst')
644
661
645 :param: path - relative to the current node
662 :param: path - relative to the current node
646
663
647 .. note::
664 .. note::
648 To access lazily (as in example above) node have to be initialized
665 To access lazily (as in example above) node have to be initialized
649 with related commit object - without it node is out of
666 with related commit object - without it node is out of
650 context and may know nothing about anything else than nearest
667 context and may know nothing about anything else than nearest
651 (located at same level) nodes.
668 (located at same level) nodes.
652 """
669 """
653 try:
670 try:
654 path = path.rstrip('/')
671 path = path.rstrip('/')
655 if path == '':
672 if path == '':
656 raise NodeError("Cannot retrieve node without path")
673 raise NodeError("Cannot retrieve node without path")
657 self.nodes # access nodes first in order to set _nodes_dict
674 self.nodes # access nodes first in order to set _nodes_dict
658 paths = path.split('/')
675 paths = path.split('/')
659 if len(paths) == 1:
676 if len(paths) == 1:
660 if not self.is_root():
677 if not self.is_root():
661 path = '/'.join((self.path, paths[0]))
678 path = '/'.join((self.path, paths[0]))
662 else:
679 else:
663 path = paths[0]
680 path = paths[0]
664 return self._nodes_dict[path]
681 return self._nodes_dict[path]
665 elif len(paths) > 1:
682 elif len(paths) > 1:
666 if self.commit is None:
683 if self.commit is None:
667 raise NodeError(
684 raise NodeError(
668 "Cannot access deeper nodes without commit")
685 "Cannot access deeper nodes without commit")
669 else:
686 else:
670 path1, path2 = paths[0], '/'.join(paths[1:])
687 path1, path2 = paths[0], '/'.join(paths[1:])
671 return self.get_node(path1).get_node(path2)
688 return self.get_node(path1).get_node(path2)
672 else:
689 else:
673 raise KeyError
690 raise KeyError
674 except KeyError:
691 except KeyError:
675 raise NodeError("Node does not exist at %s" % path)
692 raise NodeError("Node does not exist at %s" % path)
676
693
677 @LazyProperty
694 @LazyProperty
678 def state(self):
695 def state(self):
679 raise NodeError("Cannot access state of DirNode")
696 raise NodeError("Cannot access state of DirNode")
680
697
681 @LazyProperty
698 @LazyProperty
682 def size(self):
699 def size(self):
683 size = 0
700 size = 0
684 for root, dirs, files in self.commit.walk(self.path):
701 for root, dirs, files in self.commit.walk(self.path):
685 for f in files:
702 for f in files:
686 size += f.size
703 size += f.size
687
704
688 return size
705 return size
689
706
690 def __repr__(self):
707 def __repr__(self):
691 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
708 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
692 getattr(self.commit, 'short_id', ''))
709 getattr(self.commit, 'short_id', ''))
693
710
694
711
695 class RootNode(DirNode):
712 class RootNode(DirNode):
696 """
713 """
697 DirNode being the root node of the repository.
714 DirNode being the root node of the repository.
698 """
715 """
699
716
700 def __init__(self, nodes=(), commit=None):
717 def __init__(self, nodes=(), commit=None):
701 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
718 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
702
719
703 def __repr__(self):
720 def __repr__(self):
704 return '<%s>' % self.__class__.__name__
721 return '<%s>' % self.__class__.__name__
705
722
706
723
707 class SubModuleNode(Node):
724 class SubModuleNode(Node):
708 """
725 """
709 represents a SubModule of Git or SubRepo of Mercurial
726 represents a SubModule of Git or SubRepo of Mercurial
710 """
727 """
711 is_binary = False
728 is_binary = False
712 size = 0
729 size = 0
713
730
714 def __init__(self, name, url=None, commit=None, alias=None):
731 def __init__(self, name, url=None, commit=None, alias=None):
715 self.path = name
732 self.path = name
716 self.kind = NodeKind.SUBMODULE
733 self.kind = NodeKind.SUBMODULE
717 self.alias = alias
734 self.alias = alias
718
735
719 # we have to use EmptyCommit here since this can point to svn/git/hg
736 # we have to use EmptyCommit here since this can point to svn/git/hg
720 # submodules we cannot get from repository
737 # submodules we cannot get from repository
721 self.commit = EmptyCommit(str(commit), alias=alias)
738 self.commit = EmptyCommit(str(commit), alias=alias)
722 self.url = url or self._extract_submodule_url()
739 self.url = url or self._extract_submodule_url()
723
740
724 def __repr__(self):
741 def __repr__(self):
725 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
742 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
726 getattr(self.commit, 'short_id', ''))
743 getattr(self.commit, 'short_id', ''))
727
744
728 def _extract_submodule_url(self):
745 def _extract_submodule_url(self):
729 # TODO: find a way to parse gits submodule file and extract the
746 # TODO: find a way to parse gits submodule file and extract the
730 # linking URL
747 # linking URL
731 return self.path
748 return self.path
732
749
733 @LazyProperty
750 @LazyProperty
734 def name(self):
751 def name(self):
735 """
752 """
736 Returns name of the node so if its path
753 Returns name of the node so if its path
737 then only last part is returned.
754 then only last part is returned.
738 """
755 """
739 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
756 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
740 return u'%s @ %s' % (org, self.commit.short_id)
757 return u'%s @ %s' % (org, self.commit.short_id)
741
758
742
759
743 class LargeFileNode(FileNode):
760 class LargeFileNode(FileNode):
744
761
745 def _validate_path(self, path):
762 def _validate_path(self, path):
746 """
763 """
747 we override check since the LargeFileNode path is system absolute
764 we override check since the LargeFileNode path is system absolute
748 """
765 """
749
766
750 def raw_bytes(self):
767 def raw_bytes(self):
751 if self.commit:
768 if self.commit:
752 with open(self.path, 'rb') as f:
769 with open(self.path, 'rb') as f:
753 content = f.read()
770 content = f.read()
754 else:
771 else:
755 content = self._content
772 content = self._content
756 return content No newline at end of file
773 return content
General Comments 0
You need to be logged in to leave comments. Login now