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