##// END OF EJS Templates
Added diff option into git and hg changeset objects, representing git formated patch against parent1
marcink -
r2384:5563af83 beta
parent child Browse files
Show More
@@ -1,454 +1,459 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 40 self.message = safe_unicode(commit.message)
41 41 #self.branch = None
42 42 self.tags = []
43 43 self.nodes = {}
44 44 self._paths = {}
45 45
46 46 @LazyProperty
47 47 def author(self):
48 48 return safe_unicode(self._commit.committer)
49 49
50 50 @LazyProperty
51 51 def date(self):
52 52 return date_fromtimestamp(self._commit.commit_time,
53 53 self._commit.commit_timezone)
54 54
55 55 @LazyProperty
56 56 def status(self):
57 57 """
58 58 Returns modified, added, removed, deleted files for current changeset
59 59 """
60 60 return self.changed, self.added, self.removed
61 61
62 62 @LazyProperty
63 63 def branch(self):
64 64
65 65 heads = self.repository._heads(reverse=False)
66 66
67 67 ref = heads.get(self.raw_id)
68 68 if ref:
69 69 return safe_unicode(ref)
70 70
71 71 def _fix_path(self, path):
72 72 """
73 73 Paths are stored without trailing slash so we need to get rid off it if
74 74 needed.
75 75 """
76 76 if path.endswith('/'):
77 77 path = path.rstrip('/')
78 78 return path
79 79
80 80 def _get_id_for_path(self, path):
81 81
82 82 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
83 83 if not path in self._paths:
84 84 path = path.strip('/')
85 85 # set root tree
86 86 tree = self.repository._repo[self._commit.tree]
87 87 if path == '':
88 88 self._paths[''] = tree.id
89 89 return tree.id
90 90 splitted = path.split('/')
91 91 dirs, name = splitted[:-1], splitted[-1]
92 92 curdir = ''
93 93
94 94 # initially extract things from root dir
95 95 for item, stat, id in tree.iteritems():
96 96 if curdir:
97 97 name = '/'.join((curdir, item))
98 98 else:
99 99 name = item
100 100 self._paths[name] = id
101 101 self._stat_modes[name] = stat
102 102
103 103 for dir in dirs:
104 104 if curdir:
105 105 curdir = '/'.join((curdir, dir))
106 106 else:
107 107 curdir = dir
108 108 dir_id = None
109 109 for item, stat, id in tree.iteritems():
110 110 if dir == item:
111 111 dir_id = id
112 112 if dir_id:
113 113 # Update tree
114 114 tree = self.repository._repo[dir_id]
115 115 if not isinstance(tree, objects.Tree):
116 116 raise ChangesetError('%s is not a directory' % curdir)
117 117 else:
118 118 raise ChangesetError('%s have not been found' % curdir)
119 119
120 120 # cache all items from the given traversed tree
121 121 for item, stat, id in tree.iteritems():
122 122 if curdir:
123 123 name = '/'.join((curdir, item))
124 124 else:
125 125 name = item
126 126 self._paths[name] = id
127 127 self._stat_modes[name] = stat
128 128 if not path in self._paths:
129 129 raise NodeDoesNotExistError("There is no file nor directory "
130 130 "at the given path %r at revision %r"
131 131 % (path, self.short_id))
132 132 return self._paths[path]
133 133
134 134 def _get_kind(self, path):
135 135 id = self._get_id_for_path(path)
136 136 obj = self.repository._repo[id]
137 137 if isinstance(obj, objects.Blob):
138 138 return NodeKind.FILE
139 139 elif isinstance(obj, objects.Tree):
140 140 return NodeKind.DIR
141 141
142 142 def _get_file_nodes(self):
143 143 return chain(*(t[2] for t in self.walk()))
144 144
145 145 @LazyProperty
146 146 def parents(self):
147 147 """
148 148 Returns list of parents changesets.
149 149 """
150 150 return [self.repository.get_changeset(parent)
151 151 for parent in self._commit.parents]
152 152
153 153 def next(self, branch=None):
154 154
155 155 if branch and self.branch != branch:
156 156 raise VCSError('Branch option used on changeset not belonging '
157 157 'to that branch')
158 158
159 159 def _next(changeset, branch):
160 160 try:
161 161 next_ = changeset.revision + 1
162 162 next_rev = changeset.repository.revisions[next_]
163 163 except IndexError:
164 164 raise ChangesetDoesNotExistError
165 165 cs = changeset.repository.get_changeset(next_rev)
166 166
167 167 if branch and branch != cs.branch:
168 168 return _next(cs, branch)
169 169
170 170 return cs
171 171
172 172 return _next(self, branch)
173 173
174 174 def prev(self, branch=None):
175 175 if branch and self.branch != branch:
176 176 raise VCSError('Branch option used on changeset not belonging '
177 177 'to that branch')
178 178
179 179 def _prev(changeset, branch):
180 180 try:
181 181 prev_ = changeset.revision - 1
182 182 if prev_ < 0:
183 183 raise IndexError
184 184 prev_rev = changeset.repository.revisions[prev_]
185 185 except IndexError:
186 186 raise ChangesetDoesNotExistError
187 187
188 188 cs = changeset.repository.get_changeset(prev_rev)
189 189
190 190 if branch and branch != cs.branch:
191 191 return _prev(cs, branch)
192 192
193 193 return cs
194 194
195 195 return _prev(self, branch)
196 196
197 def diff(self, ignore_whitespace=True, context=3):
198 return ''.join(self.repository.get_diff(self, self.parents[0],
199 ignore_whitespace=ignore_whitespace,
200 context=context))
201
197 202 def get_file_mode(self, path):
198 203 """
199 204 Returns stat mode of the file at the given ``path``.
200 205 """
201 206 # ensure path is traversed
202 207 self._get_id_for_path(path)
203 208 return self._stat_modes[path]
204 209
205 210 def get_file_content(self, path):
206 211 """
207 212 Returns content of the file at given ``path``.
208 213 """
209 214 id = self._get_id_for_path(path)
210 215 blob = self.repository._repo[id]
211 216 return blob.as_pretty_string()
212 217
213 218 def get_file_size(self, path):
214 219 """
215 220 Returns size of the file at given ``path``.
216 221 """
217 222 id = self._get_id_for_path(path)
218 223 blob = self.repository._repo[id]
219 224 return blob.raw_length()
220 225
221 226 def get_file_changeset(self, path):
222 227 """
223 228 Returns last commit of the file at the given ``path``.
224 229 """
225 230 node = self.get_node(path)
226 231 return node.history[0]
227 232
228 233 def get_file_history(self, path):
229 234 """
230 235 Returns history of file as reversed list of ``Changeset`` objects for
231 236 which file at given ``path`` has been modified.
232 237
233 238 TODO: This function now uses os underlying 'git' and 'grep' commands
234 239 which is generally not good. Should be replaced with algorithm
235 240 iterating commits.
236 241 """
237 242 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
238 243 self.id, path
239 244 )
240 245 so, se = self.repository.run_git_command(cmd)
241 246 ids = re.findall(r'[0-9a-fA-F]{40}', so)
242 247 return [self.repository.get_changeset(id) for id in ids]
243 248
244 249 def get_file_annotate(self, path):
245 250 """
246 251 Returns a list of three element tuples with lineno,changeset and line
247 252
248 253 TODO: This function now uses os underlying 'git' command which is
249 254 generally not good. Should be replaced with algorithm iterating
250 255 commits.
251 256 """
252 257 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
253 258 # -l ==> outputs long shas (and we need all 40 characters)
254 259 # --root ==> doesn't put '^' character for bounderies
255 260 # -r sha ==> blames for the given revision
256 261 so, se = self.repository.run_git_command(cmd)
257 262 annotate = []
258 263 for i, blame_line in enumerate(so.split('\n')[:-1]):
259 264 ln_no = i + 1
260 265 id, line = re.split(r' \(.+?\) ', blame_line, 1)
261 266 annotate.append((ln_no, self.repository.get_changeset(id), line))
262 267 return annotate
263 268
264 269 def fill_archive(self, stream=None, kind='tgz', prefix=None,
265 270 subrepos=False):
266 271 """
267 272 Fills up given stream.
268 273
269 274 :param stream: file like object.
270 275 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
271 276 Default: ``tgz``.
272 277 :param prefix: name of root directory in archive.
273 278 Default is repository name and changeset's raw_id joined with dash
274 279 (``repo-tip.<KIND>``).
275 280 :param subrepos: include subrepos in this archive.
276 281
277 282 :raise ImproperArchiveTypeError: If given kind is wrong.
278 283 :raise VcsError: If given stream is None
279 284
280 285 """
281 286 allowed_kinds = settings.ARCHIVE_SPECS.keys()
282 287 if kind not in allowed_kinds:
283 288 raise ImproperArchiveTypeError('Archive kind not supported use one'
284 289 'of %s', allowed_kinds)
285 290
286 291 if prefix is None:
287 292 prefix = '%s-%s' % (self.repository.name, self.short_id)
288 293 elif prefix.startswith('/'):
289 294 raise VCSError("Prefix cannot start with leading slash")
290 295 elif prefix.strip() == '':
291 296 raise VCSError("Prefix cannot be empty")
292 297
293 298 if kind == 'zip':
294 299 frmt = 'zip'
295 300 else:
296 301 frmt = 'tar'
297 302 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
298 303 self.raw_id)
299 304 if kind == 'tgz':
300 305 cmd += ' | gzip -9'
301 306 elif kind == 'tbz2':
302 307 cmd += ' | bzip2 -9'
303 308
304 309 if stream is None:
305 310 raise VCSError('You need to pass in a valid stream for filling'
306 311 ' with archival data')
307 312 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
308 313 cwd=self.repository.path)
309 314
310 315 buffer_size = 1024 * 8
311 316 chunk = popen.stdout.read(buffer_size)
312 317 while chunk:
313 318 stream.write(chunk)
314 319 chunk = popen.stdout.read(buffer_size)
315 320 # Make sure all descriptors would be read
316 321 popen.communicate()
317 322
318 323 def get_nodes(self, path):
319 324 if self._get_kind(path) != NodeKind.DIR:
320 325 raise ChangesetError("Directory does not exist for revision %r at "
321 326 " %r" % (self.revision, path))
322 327 path = self._fix_path(path)
323 328 id = self._get_id_for_path(path)
324 329 tree = self.repository._repo[id]
325 330 dirnodes = []
326 331 filenodes = []
327 332 als = self.repository.alias
328 333 for name, stat, id in tree.iteritems():
329 334 if objects.S_ISGITLINK(stat):
330 335 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
331 336 alias=als))
332 337 continue
333 338
334 339 obj = self.repository._repo.get_object(id)
335 340 if path != '':
336 341 obj_path = '/'.join((path, name))
337 342 else:
338 343 obj_path = name
339 344 if obj_path not in self._stat_modes:
340 345 self._stat_modes[obj_path] = stat
341 346 if isinstance(obj, objects.Tree):
342 347 dirnodes.append(DirNode(obj_path, changeset=self))
343 348 elif isinstance(obj, objects.Blob):
344 349 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
345 350 else:
346 351 raise ChangesetError("Requested object should be Tree "
347 352 "or Blob, is %r" % type(obj))
348 353 nodes = dirnodes + filenodes
349 354 for node in nodes:
350 355 if not node.path in self.nodes:
351 356 self.nodes[node.path] = node
352 357 nodes.sort()
353 358 return nodes
354 359
355 360 def get_node(self, path):
356 361 if isinstance(path, unicode):
357 362 path = path.encode('utf-8')
358 363 path = self._fix_path(path)
359 364 if not path in self.nodes:
360 365 try:
361 366 id_ = self._get_id_for_path(path)
362 367 except ChangesetError:
363 368 raise NodeDoesNotExistError("Cannot find one of parents' "
364 369 "directories for a given path: %s" % path)
365 370
366 371 als = self.repository.alias
367 372 _GL = lambda m: m and objects.S_ISGITLINK(m)
368 373 if _GL(self._stat_modes.get(path)):
369 374 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
370 375 else:
371 376 obj = self.repository._repo.get_object(id_)
372 377
373 378 if isinstance(obj, objects.Tree):
374 379 if path == '':
375 380 node = RootNode(changeset=self)
376 381 else:
377 382 node = DirNode(path, changeset=self)
378 383 node._tree = obj
379 384 elif isinstance(obj, objects.Blob):
380 385 node = FileNode(path, changeset=self)
381 386 node._blob = obj
382 387 else:
383 388 raise NodeDoesNotExistError("There is no file nor directory "
384 389 "at the given path %r at revision %r"
385 390 % (path, self.short_id))
386 391 # cache node
387 392 self.nodes[path] = node
388 393 return self.nodes[path]
389 394
390 395 @LazyProperty
391 396 def affected_files(self):
392 397 """
393 398 Get's a fast accessible file changes for given changeset
394 399 """
395 400
396 401 return self.added + self.changed
397 402
398 403 @LazyProperty
399 404 def _diff_name_status(self):
400 405 output = []
401 406 for parent in self.parents:
402 407 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
403 408 so, se = self.repository.run_git_command(cmd)
404 409 output.append(so.strip())
405 410 return '\n'.join(output)
406 411
407 412 def _get_paths_for_status(self, status):
408 413 """
409 414 Returns sorted list of paths for given ``status``.
410 415
411 416 :param status: one of: *added*, *modified* or *deleted*
412 417 """
413 418 paths = set()
414 419 char = status[0].upper()
415 420 for line in self._diff_name_status.splitlines():
416 421 if not line:
417 422 continue
418 423
419 424 if line.startswith(char):
420 425 splitted = line.split(char, 1)
421 426 if not len(splitted) == 2:
422 427 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
423 428 "particularly that line: %s" % (self._diff_name_status,
424 429 line))
425 430 _path = splitted[1].strip()
426 431 paths.add(_path)
427 432 return sorted(paths)
428 433
429 434 @LazyProperty
430 435 def added(self):
431 436 """
432 437 Returns list of added ``FileNode`` objects.
433 438 """
434 439 if not self.parents:
435 440 return list(self._get_file_nodes())
436 441 return [self.get_node(path) for path in self._get_paths_for_status('added')]
437 442
438 443 @LazyProperty
439 444 def changed(self):
440 445 """
441 446 Returns list of modified ``FileNode`` objects.
442 447 """
443 448 if not self.parents:
444 449 return []
445 450 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
446 451
447 452 @LazyProperty
448 453 def removed(self):
449 454 """
450 455 Returns list of removed ``FileNode`` objects.
451 456 """
452 457 if not self.parents:
453 458 return []
454 459 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,565 +1,571 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.git
4 4 ~~~~~~~~~~~~~~~~
5 5
6 6 Git backend implementation.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import os
13 13 import re
14 14 import time
15 15 import posixpath
16 16 from dulwich.repo import Repo, NotGitRepository
17 17 #from dulwich.config import ConfigFile
18 18 from string import Template
19 19 from subprocess import Popen, PIPE
20 20 from rhodecode.lib.vcs.backends.base import BaseRepository
21 21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
22 22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
23 23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
24 24 from rhodecode.lib.vcs.exceptions import RepositoryError
25 25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
26 26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
27 27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
28 28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
30 30 from rhodecode.lib.vcs.utils.paths import abspath
31 31 from rhodecode.lib.vcs.utils.paths import get_user_home
32 32 from .workdir import GitWorkdir
33 33 from .changeset import GitChangeset
34 34 from .inmemory import GitInMemoryChangeset
35 35 from .config import ConfigFile
36 36
37 37
38 38 class GitRepository(BaseRepository):
39 39 """
40 40 Git repository backend.
41 41 """
42 42 DEFAULT_BRANCH_NAME = 'master'
43 43 scm = 'git'
44 44
45 45 def __init__(self, repo_path, create=False, src_url=None,
46 46 update_after_clone=False, bare=False):
47 47
48 48 self.path = abspath(repo_path)
49 49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
50 50 #temporary set that to now at later we will move it to constructor
51 51 baseui = None
52 52 if baseui is None:
53 53 from mercurial.ui import ui
54 54 baseui = ui()
55 55 # patch the instance of GitRepo with an "FAKE" ui object to add
56 56 # compatibility layer with Mercurial
57 57 setattr(self._repo, 'ui', baseui)
58 58
59 59 try:
60 60 self.head = self._repo.head()
61 61 except KeyError:
62 62 self.head = None
63 63
64 64 self._config_files = [
65 65 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
66 66 'config'),
67 67 abspath(get_user_home(), '.gitconfig'),
68 68 ]
69 69 self.bare = self._repo.bare
70 70
71 71 @LazyProperty
72 72 def revisions(self):
73 73 """
74 74 Returns list of revisions' ids, in ascending order. Being lazy
75 75 attribute allows external tools to inject shas from cache.
76 76 """
77 77 return self._get_all_revisions()
78 78
79 79 def run_git_command(self, cmd):
80 80 """
81 81 Runs given ``cmd`` as git command and returns tuple
82 82 (returncode, stdout, stderr).
83 83
84 84 .. note::
85 85 This method exists only until log/blame functionality is implemented
86 86 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
87 87 os command's output is road to hell...
88 88
89 89 :param cmd: git command to be executed
90 90 """
91 91
92 92 _copts = ['-c', 'core.quotepath=false', ]
93 93 _str_cmd = False
94 94 if isinstance(cmd, basestring):
95 95 cmd = [cmd]
96 96 _str_cmd = True
97 97
98 98 gitenv = os.environ
99 99 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
100 100
101 101 cmd = ['git'] + _copts + cmd
102 102 if _str_cmd:
103 103 cmd = ' '.join(cmd)
104 104 try:
105 105 opts = dict(
106 106 shell=isinstance(cmd, basestring),
107 107 stdout=PIPE,
108 108 stderr=PIPE,
109 109 env=gitenv,
110 110 )
111 111 if os.path.isdir(self.path):
112 112 opts['cwd'] = self.path
113 113 p = Popen(cmd, **opts)
114 114 except OSError, err:
115 115 raise RepositoryError("Couldn't run git command (%s).\n"
116 116 "Original error was:%s" % (cmd, err))
117 117 so, se = p.communicate()
118 118 if not se.startswith("fatal: bad default revision 'HEAD'") and \
119 119 p.returncode != 0:
120 120 raise RepositoryError("Couldn't run git command (%s).\n"
121 121 "stderr:\n%s" % (cmd, se))
122 122 return so, se
123 123
124 124 def _check_url(self, url):
125 125 """
126 126 Functon will check given url and try to verify if it's a valid
127 127 link. Sometimes it may happened that mercurial will issue basic
128 128 auth request that can cause whole API to hang when used from python
129 129 or other external calls.
130 130
131 131 On failures it'll raise urllib2.HTTPError
132 132 """
133 133
134 134 #TODO: implement this
135 135 pass
136 136
137 137 def _get_repo(self, create, src_url=None, update_after_clone=False,
138 138 bare=False):
139 139 if create and os.path.exists(self.path):
140 140 raise RepositoryError("Location already exist")
141 141 if src_url and not create:
142 142 raise RepositoryError("Create should be set to True if src_url is "
143 143 "given (clone operation creates repository)")
144 144 try:
145 145 if create and src_url:
146 146 self._check_url(src_url)
147 147 self.clone(src_url, update_after_clone, bare)
148 148 return Repo(self.path)
149 149 elif create:
150 150 os.mkdir(self.path)
151 151 if bare:
152 152 return Repo.init_bare(self.path)
153 153 else:
154 154 return Repo.init(self.path)
155 155 else:
156 156 return Repo(self.path)
157 157 except (NotGitRepository, OSError), err:
158 158 raise RepositoryError(err)
159 159
160 160 def _get_all_revisions(self):
161 161 cmd = 'rev-list --all --date-order'
162 162 try:
163 163 so, se = self.run_git_command(cmd)
164 164 except RepositoryError:
165 165 # Can be raised for empty repositories
166 166 return []
167 167 revisions = so.splitlines()
168 168 revisions.reverse()
169 169 return revisions
170 170
171 171 def _get_revision(self, revision):
172 172 """
173 173 For git backend we always return integer here. This way we ensure
174 174 that changset's revision attribute would become integer.
175 175 """
176 176 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
177 177 is_bstr = lambda o: isinstance(o, (str, unicode))
178 178 is_null = lambda o: len(o) == revision.count('0')
179 179
180 180 if len(self.revisions) == 0:
181 181 raise EmptyRepositoryError("There are no changesets yet")
182 182
183 183 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
184 184 revision = self.revisions[-1]
185 185
186 186 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
187 187 or isinstance(revision, int) or is_null(revision)):
188 188 try:
189 189 revision = self.revisions[int(revision)]
190 190 except:
191 191 raise ChangesetDoesNotExistError("Revision %r does not exist "
192 192 "for this repository %s" % (revision, self))
193 193
194 194 elif is_bstr(revision):
195 195 if not pattern.match(revision) or revision not in self.revisions:
196 196 raise ChangesetDoesNotExistError("Revision %r does not exist "
197 197 "for this repository %s" % (revision, self))
198 198
199 199 # Ensure we return full id
200 200 if not pattern.match(str(revision)):
201 201 raise ChangesetDoesNotExistError("Given revision %r not recognized"
202 202 % revision)
203 203 return revision
204 204
205 205 def _get_archives(self, archive_name='tip'):
206 206
207 207 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
208 208 yield {"type": i[0], "extension": i[1], "node": archive_name}
209 209
210 210 def _get_url(self, url):
211 211 """
212 212 Returns normalized url. If schema is not given, would fall to
213 213 filesystem (``file:///``) schema.
214 214 """
215 215 url = str(url)
216 216 if url != 'default' and not '://' in url:
217 217 url = ':///'.join(('file', url))
218 218 return url
219 219
220 220 @LazyProperty
221 221 def name(self):
222 222 return os.path.basename(self.path)
223 223
224 224 @LazyProperty
225 225 def last_change(self):
226 226 """
227 227 Returns last change made on this repository as datetime object
228 228 """
229 229 return date_fromtimestamp(self._get_mtime(), makedate()[1])
230 230
231 231 def _get_mtime(self):
232 232 try:
233 233 return time.mktime(self.get_changeset().date.timetuple())
234 234 except RepositoryError:
235 235 idx_loc = '' if self.bare else '.git'
236 236 # fallback to filesystem
237 237 in_path = os.path.join(self.path, idx_loc, "index")
238 238 he_path = os.path.join(self.path, idx_loc, "HEAD")
239 239 if os.path.exists(in_path):
240 240 return os.stat(in_path).st_mtime
241 241 else:
242 242 return os.stat(he_path).st_mtime
243 243
244 244 @LazyProperty
245 245 def description(self):
246 246 idx_loc = '' if self.bare else '.git'
247 247 undefined_description = u'unknown'
248 248 description_path = os.path.join(self.path, idx_loc, 'description')
249 249 if os.path.isfile(description_path):
250 250 return safe_unicode(open(description_path).read())
251 251 else:
252 252 return undefined_description
253 253
254 254 @LazyProperty
255 255 def contact(self):
256 256 undefined_contact = u'Unknown'
257 257 return undefined_contact
258 258
259 259 @property
260 260 def branches(self):
261 261 if not self.revisions:
262 262 return {}
263 263 refs = self._repo.refs.as_dict()
264 264 sortkey = lambda ctx: ctx[0]
265 265 _branches = [('/'.join(ref.split('/')[2:]), head)
266 266 for ref, head in refs.items()
267 267 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
268 268 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
269 269
270 270 def _heads(self, reverse=False):
271 271 refs = self._repo.get_refs()
272 272 heads = {}
273 273
274 274 for key, val in refs.items():
275 275 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
276 276 if key.startswith(ref_key):
277 277 n = key[len(ref_key):]
278 278 if n not in ['HEAD']:
279 279 heads[n] = val
280 280
281 281 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
282 282
283 283 def _get_tags(self):
284 284 if not self.revisions:
285 285 return {}
286 286 sortkey = lambda ctx: ctx[0]
287 287 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
288 288 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
289 289 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
290 290
291 291 @LazyProperty
292 292 def tags(self):
293 293 return self._get_tags()
294 294
295 295 def tag(self, name, user, revision=None, message=None, date=None,
296 296 **kwargs):
297 297 """
298 298 Creates and returns a tag for the given ``revision``.
299 299
300 300 :param name: name for new tag
301 301 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
302 302 :param revision: changeset id for which new tag would be created
303 303 :param message: message of the tag's commit
304 304 :param date: date of tag's commit
305 305
306 306 :raises TagAlreadyExistError: if tag with same name already exists
307 307 """
308 308 if name in self.tags:
309 309 raise TagAlreadyExistError("Tag %s already exists" % name)
310 310 changeset = self.get_changeset(revision)
311 311 message = message or "Added tag %s for commit %s" % (name,
312 312 changeset.raw_id)
313 313 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
314 314
315 315 self.tags = self._get_tags()
316 316 return changeset
317 317
318 318 def remove_tag(self, name, user, message=None, date=None):
319 319 """
320 320 Removes tag with the given ``name``.
321 321
322 322 :param name: name of the tag to be removed
323 323 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
324 324 :param message: message of the tag's removal commit
325 325 :param date: date of tag's removal commit
326 326
327 327 :raises TagDoesNotExistError: if tag with given name does not exists
328 328 """
329 329 if name not in self.tags:
330 330 raise TagDoesNotExistError("Tag %s does not exist" % name)
331 331 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
332 332 try:
333 333 os.remove(tagpath)
334 334 self.tags = self._get_tags()
335 335 except OSError, e:
336 336 raise RepositoryError(e.strerror)
337 337
338 338 def get_changeset(self, revision=None):
339 339 """
340 340 Returns ``GitChangeset`` object representing commit from git repository
341 341 at the given revision or head (most recent commit) if None given.
342 342 """
343 343 if isinstance(revision, GitChangeset):
344 344 return revision
345 345 revision = self._get_revision(revision)
346 346 changeset = GitChangeset(repository=self, revision=revision)
347 347 return changeset
348 348
349 349 def get_changesets(self, start=None, end=None, start_date=None,
350 350 end_date=None, branch_name=None, reverse=False):
351 351 """
352 352 Returns iterator of ``GitChangeset`` objects from start to end (both
353 353 are inclusive), in ascending date order (unless ``reverse`` is set).
354 354
355 355 :param start: changeset ID, as str; first returned changeset
356 356 :param end: changeset ID, as str; last returned changeset
357 357 :param start_date: if specified, changesets with commit date less than
358 358 ``start_date`` would be filtered out from returned set
359 359 :param end_date: if specified, changesets with commit date greater than
360 360 ``end_date`` would be filtered out from returned set
361 361 :param branch_name: if specified, changesets not reachable from given
362 362 branch would be filtered out from returned set
363 363 :param reverse: if ``True``, returned generator would be reversed
364 364 (meaning that returned changesets would have descending date order)
365 365
366 366 :raise BranchDoesNotExistError: If given ``branch_name`` does not
367 367 exist.
368 368 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
369 369 ``end`` could not be found.
370 370
371 371 """
372 372 if branch_name and branch_name not in self.branches:
373 373 raise BranchDoesNotExistError("Branch '%s' not found" \
374 374 % branch_name)
375 375 # %H at format means (full) commit hash, initial hashes are retrieved
376 376 # in ascending date order
377 377 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
378 378 cmd_params = {}
379 379 if start_date:
380 380 cmd_template += ' --since "$since"'
381 381 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
382 382 if end_date:
383 383 cmd_template += ' --until "$until"'
384 384 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
385 385 if branch_name:
386 386 cmd_template += ' $branch_name'
387 387 cmd_params['branch_name'] = branch_name
388 388 else:
389 389 cmd_template += ' --all'
390 390
391 391 cmd = Template(cmd_template).safe_substitute(**cmd_params)
392 392 revs = self.run_git_command(cmd)[0].splitlines()
393 393 start_pos = 0
394 394 end_pos = len(revs)
395 395 if start:
396 396 _start = self._get_revision(start)
397 397 try:
398 398 start_pos = revs.index(_start)
399 399 except ValueError:
400 400 pass
401 401
402 402 if end is not None:
403 403 _end = self._get_revision(end)
404 404 try:
405 405 end_pos = revs.index(_end)
406 406 except ValueError:
407 407 pass
408 408
409 409 if None not in [start, end] and start_pos > end_pos:
410 410 raise RepositoryError('start cannot be after end')
411 411
412 412 if end_pos is not None:
413 413 end_pos += 1
414 414
415 415 revs = revs[start_pos:end_pos]
416 416 if reverse:
417 417 revs = reversed(revs)
418 418 for rev in revs:
419 419 yield self.get_changeset(rev)
420 420
421 421 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
422 422 context=3):
423 423 """
424 424 Returns (git like) *diff*, as plain text. Shows changes introduced by
425 425 ``rev2`` since ``rev1``.
426 426
427 427 :param rev1: Entry point from which diff is shown. Can be
428 428 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
429 429 the changes since empty state of the repository until ``rev2``
430 430 :param rev2: Until which revision changes should be shown.
431 431 :param ignore_whitespace: If set to ``True``, would not show whitespace
432 432 changes. Defaults to ``False``.
433 433 :param context: How many lines before/after changed lines should be
434 434 shown. Defaults to ``3``.
435 435 """
436 436 flags = ['-U%s' % context]
437 437 if ignore_whitespace:
438 438 flags.append('-w')
439 439
440 if hasattr(rev1, 'raw_id'):
441 rev1 = getattr(rev1, 'raw_id')
442
443 if hasattr(rev2, 'raw_id'):
444 rev2 = getattr(rev2, 'raw_id')
445
440 446 if rev1 == self.EMPTY_CHANGESET:
441 447 rev2 = self.get_changeset(rev2).raw_id
442 448 cmd = ' '.join(['show'] + flags + [rev2])
443 449 else:
444 450 rev1 = self.get_changeset(rev1).raw_id
445 451 rev2 = self.get_changeset(rev2).raw_id
446 452 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
447 453
448 454 if path:
449 455 cmd += ' -- "%s"' % path
450 456 stdout, stderr = self.run_git_command(cmd)
451 457 # If we used 'show' command, strip first few lines (until actual diff
452 458 # starts)
453 459 if rev1 == self.EMPTY_CHANGESET:
454 460 lines = stdout.splitlines()
455 461 x = 0
456 462 for line in lines:
457 463 if line.startswith('diff'):
458 464 break
459 465 x += 1
460 466 # Append new line just like 'diff' command do
461 467 stdout = '\n'.join(lines[x:]) + '\n'
462 468 return stdout
463 469
464 470 @LazyProperty
465 471 def in_memory_changeset(self):
466 472 """
467 473 Returns ``GitInMemoryChangeset`` object for this repository.
468 474 """
469 475 return GitInMemoryChangeset(self)
470 476
471 477 def clone(self, url, update_after_clone=True, bare=False):
472 478 """
473 479 Tries to clone changes from external location.
474 480
475 481 :param update_after_clone: If set to ``False``, git won't checkout
476 482 working directory
477 483 :param bare: If set to ``True``, repository would be cloned into
478 484 *bare* git repository (no working directory at all).
479 485 """
480 486 url = self._get_url(url)
481 487 cmd = ['clone']
482 488 if bare:
483 489 cmd.append('--bare')
484 490 elif not update_after_clone:
485 491 cmd.append('--no-checkout')
486 492 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
487 493 cmd = ' '.join(cmd)
488 494 # If error occurs run_git_command raises RepositoryError already
489 495 self.run_git_command(cmd)
490 496
491 497 def pull(self, url):
492 498 """
493 499 Tries to pull changes from external location.
494 500 """
495 501 url = self._get_url(url)
496 502 cmd = ['pull']
497 503 cmd.append("--ff-only")
498 504 cmd.append(url)
499 505 cmd = ' '.join(cmd)
500 506 # If error occurs run_git_command raises RepositoryError already
501 507 self.run_git_command(cmd)
502 508
503 509 def fetch(self, url):
504 510 """
505 511 Tries to pull changes from external location.
506 512 """
507 513 url = self._get_url(url)
508 514 cmd = ['fetch']
509 515 cmd.append(url)
510 516 cmd = ' '.join(cmd)
511 517 # If error occurs run_git_command raises RepositoryError already
512 518 self.run_git_command(cmd)
513 519
514 520 @LazyProperty
515 521 def workdir(self):
516 522 """
517 523 Returns ``Workdir`` instance for this repository.
518 524 """
519 525 return GitWorkdir(self)
520 526
521 527 def get_config_value(self, section, name, config_file=None):
522 528 """
523 529 Returns configuration value for a given [``section``] and ``name``.
524 530
525 531 :param section: Section we want to retrieve value from
526 532 :param name: Name of configuration we want to retrieve
527 533 :param config_file: A path to file which should be used to retrieve
528 534 configuration from (might also be a list of file paths)
529 535 """
530 536 if config_file is None:
531 537 config_file = []
532 538 elif isinstance(config_file, basestring):
533 539 config_file = [config_file]
534 540
535 541 def gen_configs():
536 542 for path in config_file + self._config_files:
537 543 try:
538 544 yield ConfigFile.from_path(path)
539 545 except (IOError, OSError, ValueError):
540 546 continue
541 547
542 548 for config in gen_configs():
543 549 try:
544 550 return config.get(section, name)
545 551 except KeyError:
546 552 continue
547 553 return None
548 554
549 555 def get_user_name(self, config_file=None):
550 556 """
551 557 Returns user's name from global configuration file.
552 558
553 559 :param config_file: A path to file which should be used to retrieve
554 560 configuration from (might also be a list of file paths)
555 561 """
556 562 return self.get_config_value('user', 'name', config_file)
557 563
558 564 def get_user_email(self, config_file=None):
559 565 """
560 566 Returns user's email from global configuration file.
561 567
562 568 :param config_file: A path to file which should be used to retrieve
563 569 configuration from (might also be a list of file paths)
564 570 """
565 571 return self.get_config_value('user', 'email', config_file)
@@ -1,357 +1,362 b''
1 1 import os
2 2 import posixpath
3 3
4 4 from rhodecode.lib.vcs.backends.base import BaseChangeset
5 5 from rhodecode.lib.vcs.conf import settings
6 6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
7 7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
8 8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
9 9 ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
10 10 RemovedFileNodesGenerator, RootNode, SubModuleNode
11 11
12 12 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
13 13 from rhodecode.lib.vcs.utils.lazy import LazyProperty
14 14 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
15 15
16 16 from ...utils.hgcompat import archival, hex
17 17
18 18
19 19 class MercurialChangeset(BaseChangeset):
20 20 """
21 21 Represents state of the repository at the single revision.
22 22 """
23 23
24 24 def __init__(self, repository, revision):
25 25 self.repository = repository
26 26 self.raw_id = revision
27 27 self._ctx = repository._repo[revision]
28 28 self.revision = self._ctx._rev
29 29 self.nodes = {}
30 30
31 31 @LazyProperty
32 32 def tags(self):
33 33 return map(safe_unicode, self._ctx.tags())
34 34
35 35 @LazyProperty
36 36 def branch(self):
37 37 return safe_unicode(self._ctx.branch())
38 38
39 39 @LazyProperty
40 40 def bookmarks(self):
41 41 return map(safe_unicode, self._ctx.bookmarks())
42 42
43 43 @LazyProperty
44 44 def message(self):
45 45 return safe_unicode(self._ctx.description())
46 46
47 47 @LazyProperty
48 48 def author(self):
49 49 return safe_unicode(self._ctx.user())
50 50
51 51 @LazyProperty
52 52 def date(self):
53 53 return date_fromtimestamp(*self._ctx.date())
54 54
55 55 @LazyProperty
56 56 def status(self):
57 57 """
58 58 Returns modified, added, removed, deleted files for current changeset
59 59 """
60 60 return self.repository._repo.status(self._ctx.p1().node(),
61 61 self._ctx.node())
62 62
63 63 @LazyProperty
64 64 def _file_paths(self):
65 65 return list(self._ctx)
66 66
67 67 @LazyProperty
68 68 def _dir_paths(self):
69 69 p = list(set(get_dirs_for_path(*self._file_paths)))
70 70 p.insert(0, '')
71 71 return p
72 72
73 73 @LazyProperty
74 74 def _paths(self):
75 75 return self._dir_paths + self._file_paths
76 76
77 77 @LazyProperty
78 78 def id(self):
79 79 if self.last:
80 80 return u'tip'
81 81 return self.short_id
82 82
83 83 @LazyProperty
84 84 def short_id(self):
85 85 return self.raw_id[:12]
86 86
87 87 @LazyProperty
88 88 def parents(self):
89 89 """
90 90 Returns list of parents changesets.
91 91 """
92 92 return [self.repository.get_changeset(parent.rev())
93 93 for parent in self._ctx.parents() if parent.rev() >= 0]
94 94
95 95 def next(self, branch=None):
96 96
97 97 if branch and self.branch != branch:
98 98 raise VCSError('Branch option used on changeset not belonging '
99 99 'to that branch')
100 100
101 101 def _next(changeset, branch):
102 102 try:
103 103 next_ = changeset.revision + 1
104 104 next_rev = changeset.repository.revisions[next_]
105 105 except IndexError:
106 106 raise ChangesetDoesNotExistError
107 107 cs = changeset.repository.get_changeset(next_rev)
108 108
109 109 if branch and branch != cs.branch:
110 110 return _next(cs, branch)
111 111
112 112 return cs
113 113
114 114 return _next(self, branch)
115 115
116 116 def prev(self, branch=None):
117 117 if branch and self.branch != branch:
118 118 raise VCSError('Branch option used on changeset not belonging '
119 119 'to that branch')
120 120
121 121 def _prev(changeset, branch):
122 122 try:
123 123 prev_ = changeset.revision - 1
124 124 if prev_ < 0:
125 125 raise IndexError
126 126 prev_rev = changeset.repository.revisions[prev_]
127 127 except IndexError:
128 128 raise ChangesetDoesNotExistError
129 129
130 130 cs = changeset.repository.get_changeset(prev_rev)
131 131
132 132 if branch and branch != cs.branch:
133 133 return _prev(cs, branch)
134 134
135 135 return cs
136 136
137 137 return _prev(self, branch)
138 138
139 def diff(self, ignore_whitespace=True, context=3):
140 return ''.join(self._ctx.diff(git=True,
141 ignore_whitespace=ignore_whitespace,
142 context=context))
143
139 144 def _fix_path(self, path):
140 145 """
141 146 Paths are stored without trailing slash so we need to get rid off it if
142 147 needed. Also mercurial keeps filenodes as str so we need to decode
143 148 from unicode to str
144 149 """
145 150 if path.endswith('/'):
146 151 path = path.rstrip('/')
147 152
148 153 return safe_str(path)
149 154
150 155 def _get_kind(self, path):
151 156 path = self._fix_path(path)
152 157 if path in self._file_paths:
153 158 return NodeKind.FILE
154 159 elif path in self._dir_paths:
155 160 return NodeKind.DIR
156 161 else:
157 162 raise ChangesetError("Node does not exist at the given path %r"
158 163 % (path))
159 164
160 165 def _get_filectx(self, path):
161 166 path = self._fix_path(path)
162 167 if self._get_kind(path) != NodeKind.FILE:
163 168 raise ChangesetError("File does not exist for revision %r at "
164 169 " %r" % (self.revision, path))
165 170 return self._ctx.filectx(path)
166 171
167 172 def _extract_submodules(self):
168 173 """
169 174 returns a dictionary with submodule information from substate file
170 175 of hg repository
171 176 """
172 177 return self._ctx.substate
173 178
174 179 def get_file_mode(self, path):
175 180 """
176 181 Returns stat mode of the file at the given ``path``.
177 182 """
178 183 fctx = self._get_filectx(path)
179 184 if 'x' in fctx.flags():
180 185 return 0100755
181 186 else:
182 187 return 0100644
183 188
184 189 def get_file_content(self, path):
185 190 """
186 191 Returns content of the file at given ``path``.
187 192 """
188 193 fctx = self._get_filectx(path)
189 194 return fctx.data()
190 195
191 196 def get_file_size(self, path):
192 197 """
193 198 Returns size of the file at given ``path``.
194 199 """
195 200 fctx = self._get_filectx(path)
196 201 return fctx.size()
197 202
198 203 def get_file_changeset(self, path):
199 204 """
200 205 Returns last commit of the file at the given ``path``.
201 206 """
202 207 node = self.get_node(path)
203 208 return node.history[0]
204 209
205 210 def get_file_history(self, path):
206 211 """
207 212 Returns history of file as reversed list of ``Changeset`` objects for
208 213 which file at given ``path`` has been modified.
209 214 """
210 215 fctx = self._get_filectx(path)
211 216 nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
212 217 changesets = [self.repository.get_changeset(hex(node))
213 218 for node in reversed(nodes)]
214 219 return changesets
215 220
216 221 def get_file_annotate(self, path):
217 222 """
218 223 Returns a list of three element tuples with lineno,changeset and line
219 224 """
220 225 fctx = self._get_filectx(path)
221 226 annotate = []
222 227 for i, annotate_data in enumerate(fctx.annotate()):
223 228 ln_no = i + 1
224 229 annotate.append((ln_no, self.repository\
225 230 .get_changeset(hex(annotate_data[0].node())),
226 231 annotate_data[1],))
227 232
228 233 return annotate
229 234
230 235 def fill_archive(self, stream=None, kind='tgz', prefix=None,
231 236 subrepos=False):
232 237 """
233 238 Fills up given stream.
234 239
235 240 :param stream: file like object.
236 241 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
237 242 Default: ``tgz``.
238 243 :param prefix: name of root directory in archive.
239 244 Default is repository name and changeset's raw_id joined with dash
240 245 (``repo-tip.<KIND>``).
241 246 :param subrepos: include subrepos in this archive.
242 247
243 248 :raise ImproperArchiveTypeError: If given kind is wrong.
244 249 :raise VcsError: If given stream is None
245 250 """
246 251
247 252 allowed_kinds = settings.ARCHIVE_SPECS.keys()
248 253 if kind not in allowed_kinds:
249 254 raise ImproperArchiveTypeError('Archive kind not supported use one'
250 255 'of %s', allowed_kinds)
251 256
252 257 if stream is None:
253 258 raise VCSError('You need to pass in a valid stream for filling'
254 259 ' with archival data')
255 260
256 261 if prefix is None:
257 262 prefix = '%s-%s' % (self.repository.name, self.short_id)
258 263 elif prefix.startswith('/'):
259 264 raise VCSError("Prefix cannot start with leading slash")
260 265 elif prefix.strip() == '':
261 266 raise VCSError("Prefix cannot be empty")
262 267
263 268 archival.archive(self.repository._repo, stream, self.raw_id,
264 269 kind, prefix=prefix, subrepos=subrepos)
265 270
266 271 if stream.closed and hasattr(stream, 'name'):
267 272 stream = open(stream.name, 'rb')
268 273 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
269 274 stream = open(stream.name, 'rb')
270 275 else:
271 276 stream.seek(0)
272 277
273 278 def get_nodes(self, path):
274 279 """
275 280 Returns combined ``DirNode`` and ``FileNode`` objects list representing
276 281 state of changeset at the given ``path``. If node at the given ``path``
277 282 is not instance of ``DirNode``, ChangesetError would be raised.
278 283 """
279 284
280 285 if self._get_kind(path) != NodeKind.DIR:
281 286 raise ChangesetError("Directory does not exist for revision %r at "
282 287 " %r" % (self.revision, path))
283 288 path = self._fix_path(path)
284 289
285 290 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
286 291 if os.path.dirname(f) == path]
287 292 dirs = path == '' and '' or [d for d in self._dir_paths
288 293 if d and posixpath.dirname(d) == path]
289 294 dirnodes = [DirNode(d, changeset=self) for d in dirs
290 295 if os.path.dirname(d) == path]
291 296
292 297 als = self.repository.alias
293 298 for k, vals in self._extract_submodules().iteritems():
294 299 #vals = url,rev,type
295 300 loc = vals[0]
296 301 cs = vals[1]
297 302 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
298 303 alias=als))
299 304 nodes = dirnodes + filenodes
300 305 # cache nodes
301 306 for node in nodes:
302 307 self.nodes[node.path] = node
303 308 nodes.sort()
304 309
305 310 return nodes
306 311
307 312 def get_node(self, path):
308 313 """
309 314 Returns ``Node`` object from the given ``path``. If there is no node at
310 315 the given ``path``, ``ChangesetError`` would be raised.
311 316 """
312 317
313 318 path = self._fix_path(path)
314 319
315 320 if not path in self.nodes:
316 321 if path in self._file_paths:
317 322 node = FileNode(path, changeset=self)
318 323 elif path in self._dir_paths or path in self._dir_paths:
319 324 if path == '':
320 325 node = RootNode(changeset=self)
321 326 else:
322 327 node = DirNode(path, changeset=self)
323 328 else:
324 329 raise NodeDoesNotExistError("There is no file nor directory "
325 330 "at the given path: %r at revision %r"
326 331 % (path, self.short_id))
327 332 # cache node
328 333 self.nodes[path] = node
329 334 return self.nodes[path]
330 335
331 336 @LazyProperty
332 337 def affected_files(self):
333 338 """
334 339 Get's a fast accessible file changes for given changeset
335 340 """
336 341 return self._ctx.files()
337 342
338 343 @property
339 344 def added(self):
340 345 """
341 346 Returns list of added ``FileNode`` objects.
342 347 """
343 348 return AddedFileNodesGenerator([n for n in self.status[1]], self)
344 349
345 350 @property
346 351 def changed(self):
347 352 """
348 353 Returns list of modified ``FileNode`` objects.
349 354 """
350 355 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
351 356
352 357 @property
353 358 def removed(self):
354 359 """
355 360 Returns list of removed ``FileNode`` objects.
356 361 """
357 362 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
General Comments 0
You need to be logged in to leave comments. Login now