##// END OF EJS Templates
safe_unicode never fails so it's not needed to wrap it in Unicode exceptions
marcink -
r2283:d932229a codereview
parent child Browse files
Show More
@@ -1,457 +1,455 b''
1 import re
1 import re
2 from itertools import chain
2 from itertools import chain
3 from dulwich import objects
3 from dulwich import objects
4 from subprocess import Popen, PIPE
4 from subprocess import Popen, PIPE
5 from rhodecode.lib.vcs.conf import settings
5 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.exceptions import RepositoryError
6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 from rhodecode.lib.vcs.exceptions import ChangesetError
7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 from rhodecode.lib.vcs.exceptions import VCSError
9 from rhodecode.lib.vcs.exceptions import VCSError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 from rhodecode.lib.vcs.backends.base import BaseChangeset
12 from rhodecode.lib.vcs.backends.base import BaseChangeset
13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
14 RemovedFileNode, SubModuleNode
14 RemovedFileNode, SubModuleNode
15 from rhodecode.lib.vcs.utils import safe_unicode
15 from rhodecode.lib.vcs.utils import safe_unicode
16 from rhodecode.lib.vcs.utils import date_fromtimestamp
16 from rhodecode.lib.vcs.utils import date_fromtimestamp
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18
18
19
19
20 class GitChangeset(BaseChangeset):
20 class GitChangeset(BaseChangeset):
21 """
21 """
22 Represents state of the repository at single revision.
22 Represents state of the repository at single revision.
23 """
23 """
24
24
25 def __init__(self, repository, revision):
25 def __init__(self, repository, revision):
26 self._stat_modes = {}
26 self._stat_modes = {}
27 self.repository = repository
27 self.repository = repository
28 self.raw_id = revision
28 self.raw_id = revision
29 self.revision = repository.revisions.index(revision)
29 self.revision = repository.revisions.index(revision)
30
30
31 self.short_id = self.raw_id[:12]
31 self.short_id = self.raw_id[:12]
32 self.id = self.raw_id
32 self.id = self.raw_id
33 try:
33 try:
34 commit = self.repository._repo.get_object(self.raw_id)
34 commit = self.repository._repo.get_object(self.raw_id)
35 except KeyError:
35 except KeyError:
36 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
36 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
37 self._commit = commit
37 self._commit = commit
38 self._tree_id = commit.tree
38 self._tree_id = commit.tree
39
39
40 try:
40 self.message = safe_unicode(commit.message)
41 self.message = safe_unicode(commit.message)
41
42 except UnicodeDecodeError:
43 self.message = commit.message.decode(commit.encoding or 'utf-8')
44 #self.branch = None
42 #self.branch = None
45 self.tags = []
43 self.tags = []
46 self.nodes = {}
44 self.nodes = {}
47 self._paths = {}
45 self._paths = {}
48
46
49 @LazyProperty
47 @LazyProperty
50 def author(self):
48 def author(self):
51 return safe_unicode(self._commit.committer)
49 return safe_unicode(self._commit.committer)
52
50
53 @LazyProperty
51 @LazyProperty
54 def date(self):
52 def date(self):
55 return date_fromtimestamp(self._commit.commit_time,
53 return date_fromtimestamp(self._commit.commit_time,
56 self._commit.commit_timezone)
54 self._commit.commit_timezone)
57
55
58 @LazyProperty
56 @LazyProperty
59 def status(self):
57 def status(self):
60 """
58 """
61 Returns modified, added, removed, deleted files for current changeset
59 Returns modified, added, removed, deleted files for current changeset
62 """
60 """
63 return self.changed, self.added, self.removed
61 return self.changed, self.added, self.removed
64
62
65 @LazyProperty
63 @LazyProperty
66 def branch(self):
64 def branch(self):
67
65
68 heads = self.repository._heads(reverse=False)
66 heads = self.repository._heads(reverse=False)
69
67
70 ref = heads.get(self.raw_id)
68 ref = heads.get(self.raw_id)
71 if ref:
69 if ref:
72 return safe_unicode(ref)
70 return safe_unicode(ref)
73
71
74 def _fix_path(self, path):
72 def _fix_path(self, path):
75 """
73 """
76 Paths are stored without trailing slash so we need to get rid off it if
74 Paths are stored without trailing slash so we need to get rid off it if
77 needed.
75 needed.
78 """
76 """
79 if path.endswith('/'):
77 if path.endswith('/'):
80 path = path.rstrip('/')
78 path = path.rstrip('/')
81 return path
79 return path
82
80
83 def _get_id_for_path(self, path):
81 def _get_id_for_path(self, path):
84
82
85 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
83 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
86 if not path in self._paths:
84 if not path in self._paths:
87 path = path.strip('/')
85 path = path.strip('/')
88 # set root tree
86 # set root tree
89 tree = self.repository._repo[self._commit.tree]
87 tree = self.repository._repo[self._commit.tree]
90 if path == '':
88 if path == '':
91 self._paths[''] = tree.id
89 self._paths[''] = tree.id
92 return tree.id
90 return tree.id
93 splitted = path.split('/')
91 splitted = path.split('/')
94 dirs, name = splitted[:-1], splitted[-1]
92 dirs, name = splitted[:-1], splitted[-1]
95 curdir = ''
93 curdir = ''
96
94
97 # initially extract things from root dir
95 # initially extract things from root dir
98 for item, stat, id in tree.iteritems():
96 for item, stat, id in tree.iteritems():
99 if curdir:
97 if curdir:
100 name = '/'.join((curdir, item))
98 name = '/'.join((curdir, item))
101 else:
99 else:
102 name = item
100 name = item
103 self._paths[name] = id
101 self._paths[name] = id
104 self._stat_modes[name] = stat
102 self._stat_modes[name] = stat
105
103
106 for dir in dirs:
104 for dir in dirs:
107 if curdir:
105 if curdir:
108 curdir = '/'.join((curdir, dir))
106 curdir = '/'.join((curdir, dir))
109 else:
107 else:
110 curdir = dir
108 curdir = dir
111 dir_id = None
109 dir_id = None
112 for item, stat, id in tree.iteritems():
110 for item, stat, id in tree.iteritems():
113 if dir == item:
111 if dir == item:
114 dir_id = id
112 dir_id = id
115 if dir_id:
113 if dir_id:
116 # Update tree
114 # Update tree
117 tree = self.repository._repo[dir_id]
115 tree = self.repository._repo[dir_id]
118 if not isinstance(tree, objects.Tree):
116 if not isinstance(tree, objects.Tree):
119 raise ChangesetError('%s is not a directory' % curdir)
117 raise ChangesetError('%s is not a directory' % curdir)
120 else:
118 else:
121 raise ChangesetError('%s have not been found' % curdir)
119 raise ChangesetError('%s have not been found' % curdir)
122
120
123 # cache all items from the given traversed tree
121 # cache all items from the given traversed tree
124 for item, stat, id in tree.iteritems():
122 for item, stat, id in tree.iteritems():
125 if curdir:
123 if curdir:
126 name = '/'.join((curdir, item))
124 name = '/'.join((curdir, item))
127 else:
125 else:
128 name = item
126 name = item
129 self._paths[name] = id
127 self._paths[name] = id
130 self._stat_modes[name] = stat
128 self._stat_modes[name] = stat
131 if not path in self._paths:
129 if not path in self._paths:
132 raise NodeDoesNotExistError("There is no file nor directory "
130 raise NodeDoesNotExistError("There is no file nor directory "
133 "at the given path %r at revision %r"
131 "at the given path %r at revision %r"
134 % (path, self.short_id))
132 % (path, self.short_id))
135 return self._paths[path]
133 return self._paths[path]
136
134
137 def _get_kind(self, path):
135 def _get_kind(self, path):
138 id = self._get_id_for_path(path)
136 id = self._get_id_for_path(path)
139 obj = self.repository._repo[id]
137 obj = self.repository._repo[id]
140 if isinstance(obj, objects.Blob):
138 if isinstance(obj, objects.Blob):
141 return NodeKind.FILE
139 return NodeKind.FILE
142 elif isinstance(obj, objects.Tree):
140 elif isinstance(obj, objects.Tree):
143 return NodeKind.DIR
141 return NodeKind.DIR
144
142
145 def _get_file_nodes(self):
143 def _get_file_nodes(self):
146 return chain(*(t[2] for t in self.walk()))
144 return chain(*(t[2] for t in self.walk()))
147
145
148 @LazyProperty
146 @LazyProperty
149 def parents(self):
147 def parents(self):
150 """
148 """
151 Returns list of parents changesets.
149 Returns list of parents changesets.
152 """
150 """
153 return [self.repository.get_changeset(parent)
151 return [self.repository.get_changeset(parent)
154 for parent in self._commit.parents]
152 for parent in self._commit.parents]
155
153
156 def next(self, branch=None):
154 def next(self, branch=None):
157
155
158 if branch and self.branch != branch:
156 if branch and self.branch != branch:
159 raise VCSError('Branch option used on changeset not belonging '
157 raise VCSError('Branch option used on changeset not belonging '
160 'to that branch')
158 'to that branch')
161
159
162 def _next(changeset, branch):
160 def _next(changeset, branch):
163 try:
161 try:
164 next_ = changeset.revision + 1
162 next_ = changeset.revision + 1
165 next_rev = changeset.repository.revisions[next_]
163 next_rev = changeset.repository.revisions[next_]
166 except IndexError:
164 except IndexError:
167 raise ChangesetDoesNotExistError
165 raise ChangesetDoesNotExistError
168 cs = changeset.repository.get_changeset(next_rev)
166 cs = changeset.repository.get_changeset(next_rev)
169
167
170 if branch and branch != cs.branch:
168 if branch and branch != cs.branch:
171 return _next(cs, branch)
169 return _next(cs, branch)
172
170
173 return cs
171 return cs
174
172
175 return _next(self, branch)
173 return _next(self, branch)
176
174
177 def prev(self, branch=None):
175 def prev(self, branch=None):
178 if branch and self.branch != branch:
176 if branch and self.branch != branch:
179 raise VCSError('Branch option used on changeset not belonging '
177 raise VCSError('Branch option used on changeset not belonging '
180 'to that branch')
178 'to that branch')
181
179
182 def _prev(changeset, branch):
180 def _prev(changeset, branch):
183 try:
181 try:
184 prev_ = changeset.revision - 1
182 prev_ = changeset.revision - 1
185 if prev_ < 0:
183 if prev_ < 0:
186 raise IndexError
184 raise IndexError
187 prev_rev = changeset.repository.revisions[prev_]
185 prev_rev = changeset.repository.revisions[prev_]
188 except IndexError:
186 except IndexError:
189 raise ChangesetDoesNotExistError
187 raise ChangesetDoesNotExistError
190
188
191 cs = changeset.repository.get_changeset(prev_rev)
189 cs = changeset.repository.get_changeset(prev_rev)
192
190
193 if branch and branch != cs.branch:
191 if branch and branch != cs.branch:
194 return _prev(cs, branch)
192 return _prev(cs, branch)
195
193
196 return cs
194 return cs
197
195
198 return _prev(self, branch)
196 return _prev(self, branch)
199
197
200 def get_file_mode(self, path):
198 def get_file_mode(self, path):
201 """
199 """
202 Returns stat mode of the file at the given ``path``.
200 Returns stat mode of the file at the given ``path``.
203 """
201 """
204 # ensure path is traversed
202 # ensure path is traversed
205 self._get_id_for_path(path)
203 self._get_id_for_path(path)
206 return self._stat_modes[path]
204 return self._stat_modes[path]
207
205
208 def get_file_content(self, path):
206 def get_file_content(self, path):
209 """
207 """
210 Returns content of the file at given ``path``.
208 Returns content of the file at given ``path``.
211 """
209 """
212 id = self._get_id_for_path(path)
210 id = self._get_id_for_path(path)
213 blob = self.repository._repo[id]
211 blob = self.repository._repo[id]
214 return blob.as_pretty_string()
212 return blob.as_pretty_string()
215
213
216 def get_file_size(self, path):
214 def get_file_size(self, path):
217 """
215 """
218 Returns size of the file at given ``path``.
216 Returns size of the file at given ``path``.
219 """
217 """
220 id = self._get_id_for_path(path)
218 id = self._get_id_for_path(path)
221 blob = self.repository._repo[id]
219 blob = self.repository._repo[id]
222 return blob.raw_length()
220 return blob.raw_length()
223
221
224 def get_file_changeset(self, path):
222 def get_file_changeset(self, path):
225 """
223 """
226 Returns last commit of the file at the given ``path``.
224 Returns last commit of the file at the given ``path``.
227 """
225 """
228 node = self.get_node(path)
226 node = self.get_node(path)
229 return node.history[0]
227 return node.history[0]
230
228
231 def get_file_history(self, path):
229 def get_file_history(self, path):
232 """
230 """
233 Returns history of file as reversed list of ``Changeset`` objects for
231 Returns history of file as reversed list of ``Changeset`` objects for
234 which file at given ``path`` has been modified.
232 which file at given ``path`` has been modified.
235
233
236 TODO: This function now uses os underlying 'git' and 'grep' commands
234 TODO: This function now uses os underlying 'git' and 'grep' commands
237 which is generally not good. Should be replaced with algorithm
235 which is generally not good. Should be replaced with algorithm
238 iterating commits.
236 iterating commits.
239 """
237 """
240 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
238 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
241 self.id, path
239 self.id, path
242 )
240 )
243 so, se = self.repository.run_git_command(cmd)
241 so, se = self.repository.run_git_command(cmd)
244 ids = re.findall(r'[0-9a-fA-F]{40}', so)
242 ids = re.findall(r'[0-9a-fA-F]{40}', so)
245 return [self.repository.get_changeset(id) for id in ids]
243 return [self.repository.get_changeset(id) for id in ids]
246
244
247 def get_file_annotate(self, path):
245 def get_file_annotate(self, path):
248 """
246 """
249 Returns a list of three element tuples with lineno,changeset and line
247 Returns a list of three element tuples with lineno,changeset and line
250
248
251 TODO: This function now uses os underlying 'git' command which is
249 TODO: This function now uses os underlying 'git' command which is
252 generally not good. Should be replaced with algorithm iterating
250 generally not good. Should be replaced with algorithm iterating
253 commits.
251 commits.
254 """
252 """
255 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
253 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
256 # -l ==> outputs long shas (and we need all 40 characters)
254 # -l ==> outputs long shas (and we need all 40 characters)
257 # --root ==> doesn't put '^' character for bounderies
255 # --root ==> doesn't put '^' character for bounderies
258 # -r sha ==> blames for the given revision
256 # -r sha ==> blames for the given revision
259 so, se = self.repository.run_git_command(cmd)
257 so, se = self.repository.run_git_command(cmd)
260 annotate = []
258 annotate = []
261 for i, blame_line in enumerate(so.split('\n')[:-1]):
259 for i, blame_line in enumerate(so.split('\n')[:-1]):
262 ln_no = i + 1
260 ln_no = i + 1
263 id, line = re.split(r' \(.+?\) ', blame_line, 1)
261 id, line = re.split(r' \(.+?\) ', blame_line, 1)
264 annotate.append((ln_no, self.repository.get_changeset(id), line))
262 annotate.append((ln_no, self.repository.get_changeset(id), line))
265 return annotate
263 return annotate
266
264
267 def fill_archive(self, stream=None, kind='tgz', prefix=None,
265 def fill_archive(self, stream=None, kind='tgz', prefix=None,
268 subrepos=False):
266 subrepos=False):
269 """
267 """
270 Fills up given stream.
268 Fills up given stream.
271
269
272 :param stream: file like object.
270 :param stream: file like object.
273 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
271 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
274 Default: ``tgz``.
272 Default: ``tgz``.
275 :param prefix: name of root directory in archive.
273 :param prefix: name of root directory in archive.
276 Default is repository name and changeset's raw_id joined with dash
274 Default is repository name and changeset's raw_id joined with dash
277 (``repo-tip.<KIND>``).
275 (``repo-tip.<KIND>``).
278 :param subrepos: include subrepos in this archive.
276 :param subrepos: include subrepos in this archive.
279
277
280 :raise ImproperArchiveTypeError: If given kind is wrong.
278 :raise ImproperArchiveTypeError: If given kind is wrong.
281 :raise VcsError: If given stream is None
279 :raise VcsError: If given stream is None
282
280
283 """
281 """
284 allowed_kinds = settings.ARCHIVE_SPECS.keys()
282 allowed_kinds = settings.ARCHIVE_SPECS.keys()
285 if kind not in allowed_kinds:
283 if kind not in allowed_kinds:
286 raise ImproperArchiveTypeError('Archive kind not supported use one'
284 raise ImproperArchiveTypeError('Archive kind not supported use one'
287 'of %s', allowed_kinds)
285 'of %s', allowed_kinds)
288
286
289 if prefix is None:
287 if prefix is None:
290 prefix = '%s-%s' % (self.repository.name, self.short_id)
288 prefix = '%s-%s' % (self.repository.name, self.short_id)
291 elif prefix.startswith('/'):
289 elif prefix.startswith('/'):
292 raise VCSError("Prefix cannot start with leading slash")
290 raise VCSError("Prefix cannot start with leading slash")
293 elif prefix.strip() == '':
291 elif prefix.strip() == '':
294 raise VCSError("Prefix cannot be empty")
292 raise VCSError("Prefix cannot be empty")
295
293
296 if kind == 'zip':
294 if kind == 'zip':
297 frmt = 'zip'
295 frmt = 'zip'
298 else:
296 else:
299 frmt = 'tar'
297 frmt = 'tar'
300 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
298 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
301 self.raw_id)
299 self.raw_id)
302 if kind == 'tgz':
300 if kind == 'tgz':
303 cmd += ' | gzip -9'
301 cmd += ' | gzip -9'
304 elif kind == 'tbz2':
302 elif kind == 'tbz2':
305 cmd += ' | bzip2 -9'
303 cmd += ' | bzip2 -9'
306
304
307 if stream is None:
305 if stream is None:
308 raise VCSError('You need to pass in a valid stream for filling'
306 raise VCSError('You need to pass in a valid stream for filling'
309 ' with archival data')
307 ' with archival data')
310 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
308 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
311 cwd=self.repository.path)
309 cwd=self.repository.path)
312
310
313 buffer_size = 1024 * 8
311 buffer_size = 1024 * 8
314 chunk = popen.stdout.read(buffer_size)
312 chunk = popen.stdout.read(buffer_size)
315 while chunk:
313 while chunk:
316 stream.write(chunk)
314 stream.write(chunk)
317 chunk = popen.stdout.read(buffer_size)
315 chunk = popen.stdout.read(buffer_size)
318 # Make sure all descriptors would be read
316 # Make sure all descriptors would be read
319 popen.communicate()
317 popen.communicate()
320
318
321 def get_nodes(self, path):
319 def get_nodes(self, path):
322 if self._get_kind(path) != NodeKind.DIR:
320 if self._get_kind(path) != NodeKind.DIR:
323 raise ChangesetError("Directory does not exist for revision %r at "
321 raise ChangesetError("Directory does not exist for revision %r at "
324 " %r" % (self.revision, path))
322 " %r" % (self.revision, path))
325 path = self._fix_path(path)
323 path = self._fix_path(path)
326 id = self._get_id_for_path(path)
324 id = self._get_id_for_path(path)
327 tree = self.repository._repo[id]
325 tree = self.repository._repo[id]
328 dirnodes = []
326 dirnodes = []
329 filenodes = []
327 filenodes = []
330 als = self.repository.alias
328 als = self.repository.alias
331 for name, stat, id in tree.iteritems():
329 for name, stat, id in tree.iteritems():
332 if objects.S_ISGITLINK(stat):
330 if objects.S_ISGITLINK(stat):
333 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
331 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
334 alias=als))
332 alias=als))
335 continue
333 continue
336
334
337 obj = self.repository._repo.get_object(id)
335 obj = self.repository._repo.get_object(id)
338 if path != '':
336 if path != '':
339 obj_path = '/'.join((path, name))
337 obj_path = '/'.join((path, name))
340 else:
338 else:
341 obj_path = name
339 obj_path = name
342 if obj_path not in self._stat_modes:
340 if obj_path not in self._stat_modes:
343 self._stat_modes[obj_path] = stat
341 self._stat_modes[obj_path] = stat
344 if isinstance(obj, objects.Tree):
342 if isinstance(obj, objects.Tree):
345 dirnodes.append(DirNode(obj_path, changeset=self))
343 dirnodes.append(DirNode(obj_path, changeset=self))
346 elif isinstance(obj, objects.Blob):
344 elif isinstance(obj, objects.Blob):
347 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
345 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
348 else:
346 else:
349 raise ChangesetError("Requested object should be Tree "
347 raise ChangesetError("Requested object should be Tree "
350 "or Blob, is %r" % type(obj))
348 "or Blob, is %r" % type(obj))
351 nodes = dirnodes + filenodes
349 nodes = dirnodes + filenodes
352 for node in nodes:
350 for node in nodes:
353 if not node.path in self.nodes:
351 if not node.path in self.nodes:
354 self.nodes[node.path] = node
352 self.nodes[node.path] = node
355 nodes.sort()
353 nodes.sort()
356 return nodes
354 return nodes
357
355
358 def get_node(self, path):
356 def get_node(self, path):
359 if isinstance(path, unicode):
357 if isinstance(path, unicode):
360 path = path.encode('utf-8')
358 path = path.encode('utf-8')
361 path = self._fix_path(path)
359 path = self._fix_path(path)
362 if not path in self.nodes:
360 if not path in self.nodes:
363 try:
361 try:
364 id_ = self._get_id_for_path(path)
362 id_ = self._get_id_for_path(path)
365 except ChangesetError:
363 except ChangesetError:
366 raise NodeDoesNotExistError("Cannot find one of parents' "
364 raise NodeDoesNotExistError("Cannot find one of parents' "
367 "directories for a given path: %s" % path)
365 "directories for a given path: %s" % path)
368
366
369 als = self.repository.alias
367 als = self.repository.alias
370 _GL = lambda m: m and objects.S_ISGITLINK(m)
368 _GL = lambda m: m and objects.S_ISGITLINK(m)
371 if _GL(self._stat_modes.get(path)):
369 if _GL(self._stat_modes.get(path)):
372 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
370 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
373 else:
371 else:
374 obj = self.repository._repo.get_object(id_)
372 obj = self.repository._repo.get_object(id_)
375
373
376 if isinstance(obj, objects.Tree):
374 if isinstance(obj, objects.Tree):
377 if path == '':
375 if path == '':
378 node = RootNode(changeset=self)
376 node = RootNode(changeset=self)
379 else:
377 else:
380 node = DirNode(path, changeset=self)
378 node = DirNode(path, changeset=self)
381 node._tree = obj
379 node._tree = obj
382 elif isinstance(obj, objects.Blob):
380 elif isinstance(obj, objects.Blob):
383 node = FileNode(path, changeset=self)
381 node = FileNode(path, changeset=self)
384 node._blob = obj
382 node._blob = obj
385 else:
383 else:
386 raise NodeDoesNotExistError("There is no file nor directory "
384 raise NodeDoesNotExistError("There is no file nor directory "
387 "at the given path %r at revision %r"
385 "at the given path %r at revision %r"
388 % (path, self.short_id))
386 % (path, self.short_id))
389 # cache node
387 # cache node
390 self.nodes[path] = node
388 self.nodes[path] = node
391 return self.nodes[path]
389 return self.nodes[path]
392
390
393 @LazyProperty
391 @LazyProperty
394 def affected_files(self):
392 def affected_files(self):
395 """
393 """
396 Get's a fast accessible file changes for given changeset
394 Get's a fast accessible file changes for given changeset
397 """
395 """
398
396
399 return self.added + self.changed
397 return self.added + self.changed
400
398
401 @LazyProperty
399 @LazyProperty
402 def _diff_name_status(self):
400 def _diff_name_status(self):
403 output = []
401 output = []
404 for parent in self.parents:
402 for parent in self.parents:
405 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
403 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
406 so, se = self.repository.run_git_command(cmd)
404 so, se = self.repository.run_git_command(cmd)
407 output.append(so.strip())
405 output.append(so.strip())
408 return '\n'.join(output)
406 return '\n'.join(output)
409
407
410 def _get_paths_for_status(self, status):
408 def _get_paths_for_status(self, status):
411 """
409 """
412 Returns sorted list of paths for given ``status``.
410 Returns sorted list of paths for given ``status``.
413
411
414 :param status: one of: *added*, *modified* or *deleted*
412 :param status: one of: *added*, *modified* or *deleted*
415 """
413 """
416 paths = set()
414 paths = set()
417 char = status[0].upper()
415 char = status[0].upper()
418 for line in self._diff_name_status.splitlines():
416 for line in self._diff_name_status.splitlines():
419 if not line:
417 if not line:
420 continue
418 continue
421
419
422 if line.startswith(char):
420 if line.startswith(char):
423 splitted = line.split(char, 1)
421 splitted = line.split(char, 1)
424 if not len(splitted) == 2:
422 if not len(splitted) == 2:
425 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
423 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
426 "particularly that line: %s" % (self._diff_name_status,
424 "particularly that line: %s" % (self._diff_name_status,
427 line))
425 line))
428 _path = splitted[1].strip()
426 _path = splitted[1].strip()
429 paths.add(_path)
427 paths.add(_path)
430 return sorted(paths)
428 return sorted(paths)
431
429
432 @LazyProperty
430 @LazyProperty
433 def added(self):
431 def added(self):
434 """
432 """
435 Returns list of added ``FileNode`` objects.
433 Returns list of added ``FileNode`` objects.
436 """
434 """
437 if not self.parents:
435 if not self.parents:
438 return list(self._get_file_nodes())
436 return list(self._get_file_nodes())
439 return [self.get_node(path) for path in self._get_paths_for_status('added')]
437 return [self.get_node(path) for path in self._get_paths_for_status('added')]
440
438
441 @LazyProperty
439 @LazyProperty
442 def changed(self):
440 def changed(self):
443 """
441 """
444 Returns list of modified ``FileNode`` objects.
442 Returns list of modified ``FileNode`` objects.
445 """
443 """
446 if not self.parents:
444 if not self.parents:
447 return []
445 return []
448 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
446 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
449
447
450 @LazyProperty
448 @LazyProperty
451 def removed(self):
449 def removed(self):
452 """
450 """
453 Returns list of removed ``FileNode`` objects.
451 Returns list of removed ``FileNode`` objects.
454 """
452 """
455 if not self.parents:
453 if not self.parents:
456 return []
454 return []
457 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
455 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
General Comments 0
You need to be logged in to leave comments. Login now