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