##// END OF EJS Templates
Synced vcs with upstream
marcink -
r2543:03a77098 beta
parent child Browse files
Show More
@@ -1,41 +1,41 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs
4 4 ~~~
5 5
6 6 Various version Control System (vcs) management abstraction layer for
7 7 Python.
8 8
9 9 :created_on: Apr 8, 2010
10 10 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
11 11 """
12 12
13 VERSION = (0, 2, 3, 'dev')
13 VERSION = (0, 3, 0, 'dev')
14 14
15 15 __version__ = '.'.join((str(each) for each in VERSION[:4]))
16 16
17 17 __all__ = [
18 18 'get_version', 'get_repo', 'get_backend',
19 19 'VCSError', 'RepositoryError', 'ChangesetError']
20 20
21 21 import sys
22 22 from rhodecode.lib.vcs.backends import get_repo, get_backend
23 23 from rhodecode.lib.vcs.exceptions import VCSError, RepositoryError, ChangesetError
24 24
25 25
26 26 def get_version():
27 27 """
28 28 Returns shorter version (digit parts only) as string.
29 29 """
30 30 return '.'.join((str(each) for each in VERSION[:3]))
31 31
32 32 def main(argv=None):
33 33 if argv is None:
34 34 argv = sys.argv
35 35 from rhodecode.lib.vcs.cli import ExecutionManager
36 36 manager = ExecutionManager(argv)
37 37 manager.execute()
38 38 return 0
39 39
40 40 if __name__ == '__main__':
41 41 sys.exit(main(sys.argv))
@@ -1,469 +1,468 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 from dulwich.objects import Commit, Tag
19 18
20 19
21 20 class GitChangeset(BaseChangeset):
22 21 """
23 22 Represents state of the repository at single revision.
24 23 """
25 24
26 25 def __init__(self, repository, revision):
27 26 self._stat_modes = {}
28 27 self.repository = repository
29 28
30 29 try:
31 30 commit = self.repository._repo.get_object(revision)
32 if isinstance(commit, Tag):
31 if isinstance(commit, objects.Tag):
33 32 revision = commit.object[1]
34 33 commit = self.repository._repo.get_object(commit.object[1])
35 34 except KeyError:
36 35 raise RepositoryError("Cannot get object with id %s" % revision)
37 36 self.raw_id = revision
38 37 self.id = self.raw_id
39 38 self.short_id = self.raw_id[:12]
40 39 self._commit = commit
41 40
42 41 self._tree_id = commit.tree
43 42 self._commiter_property = 'committer'
44 43 self._date_property = 'commit_time'
45 44 self._date_tz_property = 'commit_timezone'
46 45 self.revision = repository.revisions.index(revision)
47 46
48 47 self.message = safe_unicode(commit.message)
49 48 #self.branch = None
50 49 self.tags = []
51 50 self.nodes = {}
52 51 self._paths = {}
53 52
54 53 @LazyProperty
55 54 def author(self):
56 55 return safe_unicode(getattr(self._commit, self._commiter_property))
57 56
58 57 @LazyProperty
59 58 def date(self):
60 59 return date_fromtimestamp(getattr(self._commit, self._date_property),
61 60 getattr(self._commit, self._date_tz_property))
62 61
63 62 @LazyProperty
64 63 def status(self):
65 64 """
66 65 Returns modified, added, removed, deleted files for current changeset
67 66 """
68 67 return self.changed, self.added, self.removed
69 68
70 69 @LazyProperty
71 70 def branch(self):
72 71
73 72 heads = self.repository._heads(reverse=False)
74 73
75 74 ref = heads.get(self.raw_id)
76 75 if ref:
77 76 return safe_unicode(ref)
78 77
79 78 def _fix_path(self, path):
80 79 """
81 80 Paths are stored without trailing slash so we need to get rid off it if
82 81 needed.
83 82 """
84 83 if path.endswith('/'):
85 84 path = path.rstrip('/')
86 85 return path
87 86
88 87 def _get_id_for_path(self, path):
89 88
90 89 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
91 90 if not path in self._paths:
92 91 path = path.strip('/')
93 92 # set root tree
94 93 tree = self.repository._repo[self._tree_id]
95 94 if path == '':
96 95 self._paths[''] = tree.id
97 96 return tree.id
98 97 splitted = path.split('/')
99 98 dirs, name = splitted[:-1], splitted[-1]
100 99 curdir = ''
101 100
102 101 # initially extract things from root dir
103 102 for item, stat, id in tree.iteritems():
104 103 if curdir:
105 104 name = '/'.join((curdir, item))
106 105 else:
107 106 name = item
108 107 self._paths[name] = id
109 108 self._stat_modes[name] = stat
110 109
111 110 for dir in dirs:
112 111 if curdir:
113 112 curdir = '/'.join((curdir, dir))
114 113 else:
115 114 curdir = dir
116 115 dir_id = None
117 116 for item, stat, id in tree.iteritems():
118 117 if dir == item:
119 118 dir_id = id
120 119 if dir_id:
121 120 # Update tree
122 121 tree = self.repository._repo[dir_id]
123 122 if not isinstance(tree, objects.Tree):
124 123 raise ChangesetError('%s is not a directory' % curdir)
125 124 else:
126 125 raise ChangesetError('%s have not been found' % curdir)
127 126
128 127 # cache all items from the given traversed tree
129 128 for item, stat, id in tree.iteritems():
130 129 if curdir:
131 130 name = '/'.join((curdir, item))
132 131 else:
133 132 name = item
134 133 self._paths[name] = id
135 134 self._stat_modes[name] = stat
136 135 if not path in self._paths:
137 136 raise NodeDoesNotExistError("There is no file nor directory "
138 137 "at the given path %r at revision %r"
139 138 % (path, self.short_id))
140 139 return self._paths[path]
141 140
142 141 def _get_kind(self, path):
143 142 obj = self.repository._repo[self._get_id_for_path(path)]
144 143 if isinstance(obj, objects.Blob):
145 144 return NodeKind.FILE
146 145 elif isinstance(obj, objects.Tree):
147 146 return NodeKind.DIR
148 147
149 148 def _get_file_nodes(self):
150 149 return chain(*(t[2] for t in self.walk()))
151 150
152 151 @LazyProperty
153 152 def parents(self):
154 153 """
155 154 Returns list of parents changesets.
156 155 """
157 156 return [self.repository.get_changeset(parent)
158 157 for parent in self._commit.parents]
159 158
160 159 def next(self, branch=None):
161 160
162 161 if branch and self.branch != branch:
163 162 raise VCSError('Branch option used on changeset not belonging '
164 163 'to that branch')
165 164
166 165 def _next(changeset, branch):
167 166 try:
168 167 next_ = changeset.revision + 1
169 168 next_rev = changeset.repository.revisions[next_]
170 169 except IndexError:
171 170 raise ChangesetDoesNotExistError
172 171 cs = changeset.repository.get_changeset(next_rev)
173 172
174 173 if branch and branch != cs.branch:
175 174 return _next(cs, branch)
176 175
177 176 return cs
178 177
179 178 return _next(self, branch)
180 179
181 180 def prev(self, branch=None):
182 181 if branch and self.branch != branch:
183 182 raise VCSError('Branch option used on changeset not belonging '
184 183 'to that branch')
185 184
186 185 def _prev(changeset, branch):
187 186 try:
188 187 prev_ = changeset.revision - 1
189 188 if prev_ < 0:
190 189 raise IndexError
191 190 prev_rev = changeset.repository.revisions[prev_]
192 191 except IndexError:
193 192 raise ChangesetDoesNotExistError
194 193
195 194 cs = changeset.repository.get_changeset(prev_rev)
196 195
197 196 if branch and branch != cs.branch:
198 197 return _prev(cs, branch)
199 198
200 199 return cs
201 200
202 201 return _prev(self, branch)
203 202
204 203 def diff(self, ignore_whitespace=True, context=3):
205 204 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
206 205 rev2 = self
207 206 return ''.join(self.repository.get_diff(rev1, rev2,
208 207 ignore_whitespace=ignore_whitespace,
209 208 context=context))
210 209
211 210 def get_file_mode(self, path):
212 211 """
213 212 Returns stat mode of the file at the given ``path``.
214 213 """
215 214 # ensure path is traversed
216 215 self._get_id_for_path(path)
217 216 return self._stat_modes[path]
218 217
219 218 def get_file_content(self, path):
220 219 """
221 220 Returns content of the file at given ``path``.
222 221 """
223 222 id = self._get_id_for_path(path)
224 223 blob = self.repository._repo[id]
225 224 return blob.as_pretty_string()
226 225
227 226 def get_file_size(self, path):
228 227 """
229 228 Returns size of the file at given ``path``.
230 229 """
231 230 id = self._get_id_for_path(path)
232 231 blob = self.repository._repo[id]
233 232 return blob.raw_length()
234 233
235 234 def get_file_changeset(self, path):
236 235 """
237 236 Returns last commit of the file at the given ``path``.
238 237 """
239 238 node = self.get_node(path)
240 239 return node.history[0]
241 240
242 241 def get_file_history(self, path):
243 242 """
244 243 Returns history of file as reversed list of ``Changeset`` objects for
245 244 which file at given ``path`` has been modified.
246 245
247 246 TODO: This function now uses os underlying 'git' and 'grep' commands
248 247 which is generally not good. Should be replaced with algorithm
249 248 iterating commits.
250 249 """
251 250 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
252 251 self.id, path
253 252 )
254 253 so, se = self.repository.run_git_command(cmd)
255 254 ids = re.findall(r'[0-9a-fA-F]{40}', so)
256 255 return [self.repository.get_changeset(id) for id in ids]
257 256
258 257 def get_file_annotate(self, path):
259 258 """
260 259 Returns a list of three element tuples with lineno,changeset and line
261 260
262 261 TODO: This function now uses os underlying 'git' command which is
263 262 generally not good. Should be replaced with algorithm iterating
264 263 commits.
265 264 """
266 265 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
267 266 # -l ==> outputs long shas (and we need all 40 characters)
268 267 # --root ==> doesn't put '^' character for bounderies
269 268 # -r sha ==> blames for the given revision
270 269 so, se = self.repository.run_git_command(cmd)
271 270
272 271 annotate = []
273 272 for i, blame_line in enumerate(so.split('\n')[:-1]):
274 273 ln_no = i + 1
275 274 id, line = re.split(r' ', blame_line, 1)
276 275 annotate.append((ln_no, self.repository.get_changeset(id), line))
277 276 return annotate
278 277
279 278 def fill_archive(self, stream=None, kind='tgz', prefix=None,
280 279 subrepos=False):
281 280 """
282 281 Fills up given stream.
283 282
284 283 :param stream: file like object.
285 284 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
286 285 Default: ``tgz``.
287 286 :param prefix: name of root directory in archive.
288 287 Default is repository name and changeset's raw_id joined with dash
289 288 (``repo-tip.<KIND>``).
290 289 :param subrepos: include subrepos in this archive.
291 290
292 291 :raise ImproperArchiveTypeError: If given kind is wrong.
293 292 :raise VcsError: If given stream is None
294 293
295 294 """
296 295 allowed_kinds = settings.ARCHIVE_SPECS.keys()
297 296 if kind not in allowed_kinds:
298 297 raise ImproperArchiveTypeError('Archive kind not supported use one'
299 298 'of %s', allowed_kinds)
300 299
301 300 if prefix is None:
302 301 prefix = '%s-%s' % (self.repository.name, self.short_id)
303 302 elif prefix.startswith('/'):
304 303 raise VCSError("Prefix cannot start with leading slash")
305 304 elif prefix.strip() == '':
306 305 raise VCSError("Prefix cannot be empty")
307 306
308 307 if kind == 'zip':
309 308 frmt = 'zip'
310 309 else:
311 310 frmt = 'tar'
312 311 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
313 312 self.raw_id)
314 313 if kind == 'tgz':
315 314 cmd += ' | gzip -9'
316 315 elif kind == 'tbz2':
317 316 cmd += ' | bzip2 -9'
318 317
319 318 if stream is None:
320 319 raise VCSError('You need to pass in a valid stream for filling'
321 320 ' with archival data')
322 321 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
323 322 cwd=self.repository.path)
324 323
325 324 buffer_size = 1024 * 8
326 325 chunk = popen.stdout.read(buffer_size)
327 326 while chunk:
328 327 stream.write(chunk)
329 328 chunk = popen.stdout.read(buffer_size)
330 329 # Make sure all descriptors would be read
331 330 popen.communicate()
332 331
333 332 def get_nodes(self, path):
334 333 if self._get_kind(path) != NodeKind.DIR:
335 334 raise ChangesetError("Directory does not exist for revision %r at "
336 335 " %r" % (self.revision, path))
337 336 path = self._fix_path(path)
338 337 id = self._get_id_for_path(path)
339 338 tree = self.repository._repo[id]
340 339 dirnodes = []
341 340 filenodes = []
342 341 als = self.repository.alias
343 342 for name, stat, id in tree.iteritems():
344 343 if objects.S_ISGITLINK(stat):
345 344 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
346 345 alias=als))
347 346 continue
348 347
349 348 obj = self.repository._repo.get_object(id)
350 349 if path != '':
351 350 obj_path = '/'.join((path, name))
352 351 else:
353 352 obj_path = name
354 353 if obj_path not in self._stat_modes:
355 354 self._stat_modes[obj_path] = stat
356 355 if isinstance(obj, objects.Tree):
357 356 dirnodes.append(DirNode(obj_path, changeset=self))
358 357 elif isinstance(obj, objects.Blob):
359 358 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
360 359 else:
361 360 raise ChangesetError("Requested object should be Tree "
362 361 "or Blob, is %r" % type(obj))
363 362 nodes = dirnodes + filenodes
364 363 for node in nodes:
365 364 if not node.path in self.nodes:
366 365 self.nodes[node.path] = node
367 366 nodes.sort()
368 367 return nodes
369 368
370 369 def get_node(self, path):
371 370 if isinstance(path, unicode):
372 371 path = path.encode('utf-8')
373 372 path = self._fix_path(path)
374 373 if not path in self.nodes:
375 374 try:
376 375 id_ = self._get_id_for_path(path)
377 376 except ChangesetError:
378 377 raise NodeDoesNotExistError("Cannot find one of parents' "
379 378 "directories for a given path: %s" % path)
380 379
381 380 _GL = lambda m: m and objects.S_ISGITLINK(m)
382 381 if _GL(self._stat_modes.get(path)):
383 382 node = SubModuleNode(path, url=None, changeset=id_,
384 383 alias=self.repository.alias)
385 384 else:
386 385 obj = self.repository._repo.get_object(id_)
387 386
388 387 if isinstance(obj, objects.Tree):
389 388 if path == '':
390 389 node = RootNode(changeset=self)
391 390 else:
392 391 node = DirNode(path, changeset=self)
393 392 node._tree = obj
394 393 elif isinstance(obj, objects.Blob):
395 394 node = FileNode(path, changeset=self)
396 395 node._blob = obj
397 396 else:
398 397 raise NodeDoesNotExistError("There is no file nor directory "
399 398 "at the given path %r at revision %r"
400 399 % (path, self.short_id))
401 400 # cache node
402 401 self.nodes[path] = node
403 402 return self.nodes[path]
404 403
405 404 @LazyProperty
406 405 def affected_files(self):
407 406 """
408 407 Get's a fast accessible file changes for given changeset
409 408 """
410 409
411 410 return self.added + self.changed
412 411
413 412 @LazyProperty
414 413 def _diff_name_status(self):
415 414 output = []
416 415 for parent in self.parents:
417 416 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
418 417 so, se = self.repository.run_git_command(cmd)
419 418 output.append(so.strip())
420 419 return '\n'.join(output)
421 420
422 421 def _get_paths_for_status(self, status):
423 422 """
424 423 Returns sorted list of paths for given ``status``.
425 424
426 425 :param status: one of: *added*, *modified* or *deleted*
427 426 """
428 427 paths = set()
429 428 char = status[0].upper()
430 429 for line in self._diff_name_status.splitlines():
431 430 if not line:
432 431 continue
433 432
434 433 if line.startswith(char):
435 434 splitted = line.split(char, 1)
436 435 if not len(splitted) == 2:
437 436 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
438 437 "particularly that line: %s" % (self._diff_name_status,
439 438 line))
440 439 _path = splitted[1].strip()
441 440 paths.add(_path)
442 441 return sorted(paths)
443 442
444 443 @LazyProperty
445 444 def added(self):
446 445 """
447 446 Returns list of added ``FileNode`` objects.
448 447 """
449 448 if not self.parents:
450 449 return list(self._get_file_nodes())
451 450 return [self.get_node(path) for path in self._get_paths_for_status('added')]
452 451
453 452 @LazyProperty
454 453 def changed(self):
455 454 """
456 455 Returns list of modified ``FileNode`` objects.
457 456 """
458 457 if not self.parents:
459 458 return []
460 459 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
461 460
462 461 @LazyProperty
463 462 def removed(self):
464 463 """
465 464 Returns list of removed ``FileNode`` objects.
466 465 """
467 466 if not self.parents:
468 467 return []
469 468 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,362 +1,361 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
16 from ...utils.hgcompat import archival, hex
15 from rhodecode.lib.vcs.utils.hgcompat import archival, hex
17 16
18 17
19 18 class MercurialChangeset(BaseChangeset):
20 19 """
21 20 Represents state of the repository at the single revision.
22 21 """
23 22
24 23 def __init__(self, repository, revision):
25 24 self.repository = repository
26 25 self.raw_id = revision
27 26 self._ctx = repository._repo[revision]
28 27 self.revision = self._ctx._rev
29 28 self.nodes = {}
30 29
31 30 @LazyProperty
32 31 def tags(self):
33 32 return map(safe_unicode, self._ctx.tags())
34 33
35 34 @LazyProperty
36 35 def branch(self):
37 36 return safe_unicode(self._ctx.branch())
38 37
39 38 @LazyProperty
40 39 def bookmarks(self):
41 40 return map(safe_unicode, self._ctx.bookmarks())
42 41
43 42 @LazyProperty
44 43 def message(self):
45 44 return safe_unicode(self._ctx.description())
46 45
47 46 @LazyProperty
48 47 def author(self):
49 48 return safe_unicode(self._ctx.user())
50 49
51 50 @LazyProperty
52 51 def date(self):
53 52 return date_fromtimestamp(*self._ctx.date())
54 53
55 54 @LazyProperty
56 55 def status(self):
57 56 """
58 57 Returns modified, added, removed, deleted files for current changeset
59 58 """
60 59 return self.repository._repo.status(self._ctx.p1().node(),
61 60 self._ctx.node())
62 61
63 62 @LazyProperty
64 63 def _file_paths(self):
65 64 return list(self._ctx)
66 65
67 66 @LazyProperty
68 67 def _dir_paths(self):
69 68 p = list(set(get_dirs_for_path(*self._file_paths)))
70 69 p.insert(0, '')
71 70 return p
72 71
73 72 @LazyProperty
74 73 def _paths(self):
75 74 return self._dir_paths + self._file_paths
76 75
77 76 @LazyProperty
78 77 def id(self):
79 78 if self.last:
80 79 return u'tip'
81 80 return self.short_id
82 81
83 82 @LazyProperty
84 83 def short_id(self):
85 84 return self.raw_id[:12]
86 85
87 86 @LazyProperty
88 87 def parents(self):
89 88 """
90 89 Returns list of parents changesets.
91 90 """
92 91 return [self.repository.get_changeset(parent.rev())
93 92 for parent in self._ctx.parents() if parent.rev() >= 0]
94 93
95 94 def next(self, branch=None):
96 95
97 96 if branch and self.branch != branch:
98 97 raise VCSError('Branch option used on changeset not belonging '
99 98 'to that branch')
100 99
101 100 def _next(changeset, branch):
102 101 try:
103 102 next_ = changeset.revision + 1
104 103 next_rev = changeset.repository.revisions[next_]
105 104 except IndexError:
106 105 raise ChangesetDoesNotExistError
107 106 cs = changeset.repository.get_changeset(next_rev)
108 107
109 108 if branch and branch != cs.branch:
110 109 return _next(cs, branch)
111 110
112 111 return cs
113 112
114 113 return _next(self, branch)
115 114
116 115 def prev(self, branch=None):
117 116 if branch and self.branch != branch:
118 117 raise VCSError('Branch option used on changeset not belonging '
119 118 'to that branch')
120 119
121 120 def _prev(changeset, branch):
122 121 try:
123 122 prev_ = changeset.revision - 1
124 123 if prev_ < 0:
125 124 raise IndexError
126 125 prev_rev = changeset.repository.revisions[prev_]
127 126 except IndexError:
128 127 raise ChangesetDoesNotExistError
129 128
130 129 cs = changeset.repository.get_changeset(prev_rev)
131 130
132 131 if branch and branch != cs.branch:
133 132 return _prev(cs, branch)
134 133
135 134 return cs
136 135
137 136 return _prev(self, branch)
138 137
139 138 def diff(self, ignore_whitespace=True, context=3):
140 139 return ''.join(self._ctx.diff(git=True,
141 140 ignore_whitespace=ignore_whitespace,
142 141 context=context))
143 142
144 143 def _fix_path(self, path):
145 144 """
146 145 Paths are stored without trailing slash so we need to get rid off it if
147 146 needed. Also mercurial keeps filenodes as str so we need to decode
148 147 from unicode to str
149 148 """
150 149 if path.endswith('/'):
151 150 path = path.rstrip('/')
152 151
153 152 return safe_str(path)
154 153
155 154 def _get_kind(self, path):
156 155 path = self._fix_path(path)
157 156 if path in self._file_paths:
158 157 return NodeKind.FILE
159 158 elif path in self._dir_paths:
160 159 return NodeKind.DIR
161 160 else:
162 161 raise ChangesetError("Node does not exist at the given path %r"
163 162 % (path))
164 163
165 164 def _get_filectx(self, path):
166 165 path = self._fix_path(path)
167 166 if self._get_kind(path) != NodeKind.FILE:
168 167 raise ChangesetError("File does not exist for revision %r at "
169 168 " %r" % (self.revision, path))
170 169 return self._ctx.filectx(path)
171 170
172 171 def _extract_submodules(self):
173 172 """
174 173 returns a dictionary with submodule information from substate file
175 174 of hg repository
176 175 """
177 176 return self._ctx.substate
178 177
179 178 def get_file_mode(self, path):
180 179 """
181 180 Returns stat mode of the file at the given ``path``.
182 181 """
183 182 fctx = self._get_filectx(path)
184 183 if 'x' in fctx.flags():
185 184 return 0100755
186 185 else:
187 186 return 0100644
188 187
189 188 def get_file_content(self, path):
190 189 """
191 190 Returns content of the file at given ``path``.
192 191 """
193 192 fctx = self._get_filectx(path)
194 193 return fctx.data()
195 194
196 195 def get_file_size(self, path):
197 196 """
198 197 Returns size of the file at given ``path``.
199 198 """
200 199 fctx = self._get_filectx(path)
201 200 return fctx.size()
202 201
203 202 def get_file_changeset(self, path):
204 203 """
205 204 Returns last commit of the file at the given ``path``.
206 205 """
207 206 node = self.get_node(path)
208 207 return node.history[0]
209 208
210 209 def get_file_history(self, path):
211 210 """
212 211 Returns history of file as reversed list of ``Changeset`` objects for
213 212 which file at given ``path`` has been modified.
214 213 """
215 214 fctx = self._get_filectx(path)
216 215 nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
217 216 changesets = [self.repository.get_changeset(hex(node))
218 217 for node in reversed(nodes)]
219 218 return changesets
220 219
221 220 def get_file_annotate(self, path):
222 221 """
223 222 Returns a list of three element tuples with lineno,changeset and line
224 223 """
225 224 fctx = self._get_filectx(path)
226 225 annotate = []
227 226 for i, annotate_data in enumerate(fctx.annotate()):
228 227 ln_no = i + 1
229 228 annotate.append((ln_no, self.repository\
230 229 .get_changeset(hex(annotate_data[0].node())),
231 230 annotate_data[1],))
232 231
233 232 return annotate
234 233
235 234 def fill_archive(self, stream=None, kind='tgz', prefix=None,
236 235 subrepos=False):
237 236 """
238 237 Fills up given stream.
239 238
240 239 :param stream: file like object.
241 240 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
242 241 Default: ``tgz``.
243 242 :param prefix: name of root directory in archive.
244 243 Default is repository name and changeset's raw_id joined with dash
245 244 (``repo-tip.<KIND>``).
246 245 :param subrepos: include subrepos in this archive.
247 246
248 247 :raise ImproperArchiveTypeError: If given kind is wrong.
249 248 :raise VcsError: If given stream is None
250 249 """
251 250
252 251 allowed_kinds = settings.ARCHIVE_SPECS.keys()
253 252 if kind not in allowed_kinds:
254 253 raise ImproperArchiveTypeError('Archive kind not supported use one'
255 254 'of %s', allowed_kinds)
256 255
257 256 if stream is None:
258 257 raise VCSError('You need to pass in a valid stream for filling'
259 258 ' with archival data')
260 259
261 260 if prefix is None:
262 261 prefix = '%s-%s' % (self.repository.name, self.short_id)
263 262 elif prefix.startswith('/'):
264 263 raise VCSError("Prefix cannot start with leading slash")
265 264 elif prefix.strip() == '':
266 265 raise VCSError("Prefix cannot be empty")
267 266
268 267 archival.archive(self.repository._repo, stream, self.raw_id,
269 268 kind, prefix=prefix, subrepos=subrepos)
270 269
271 270 if stream.closed and hasattr(stream, 'name'):
272 271 stream = open(stream.name, 'rb')
273 272 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
274 273 stream = open(stream.name, 'rb')
275 274 else:
276 275 stream.seek(0)
277 276
278 277 def get_nodes(self, path):
279 278 """
280 279 Returns combined ``DirNode`` and ``FileNode`` objects list representing
281 280 state of changeset at the given ``path``. If node at the given ``path``
282 281 is not instance of ``DirNode``, ChangesetError would be raised.
283 282 """
284 283
285 284 if self._get_kind(path) != NodeKind.DIR:
286 285 raise ChangesetError("Directory does not exist for revision %r at "
287 286 " %r" % (self.revision, path))
288 287 path = self._fix_path(path)
289 288
290 289 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
291 290 if os.path.dirname(f) == path]
292 291 dirs = path == '' and '' or [d for d in self._dir_paths
293 292 if d and posixpath.dirname(d) == path]
294 293 dirnodes = [DirNode(d, changeset=self) for d in dirs
295 294 if os.path.dirname(d) == path]
296 295
297 296 als = self.repository.alias
298 297 for k, vals in self._extract_submodules().iteritems():
299 298 #vals = url,rev,type
300 299 loc = vals[0]
301 300 cs = vals[1]
302 301 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
303 302 alias=als))
304 303 nodes = dirnodes + filenodes
305 304 # cache nodes
306 305 for node in nodes:
307 306 self.nodes[node.path] = node
308 307 nodes.sort()
309 308
310 309 return nodes
311 310
312 311 def get_node(self, path):
313 312 """
314 313 Returns ``Node`` object from the given ``path``. If there is no node at
315 314 the given ``path``, ``ChangesetError`` would be raised.
316 315 """
317 316
318 317 path = self._fix_path(path)
319 318
320 319 if not path in self.nodes:
321 320 if path in self._file_paths:
322 321 node = FileNode(path, changeset=self)
323 322 elif path in self._dir_paths or path in self._dir_paths:
324 323 if path == '':
325 324 node = RootNode(changeset=self)
326 325 else:
327 326 node = DirNode(path, changeset=self)
328 327 else:
329 328 raise NodeDoesNotExistError("There is no file nor directory "
330 329 "at the given path: %r at revision %r"
331 330 % (path, self.short_id))
332 331 # cache node
333 332 self.nodes[path] = node
334 333 return self.nodes[path]
335 334
336 335 @LazyProperty
337 336 def affected_files(self):
338 337 """
339 338 Get's a fast accessible file changes for given changeset
340 339 """
341 340 return self._ctx.files()
342 341
343 342 @property
344 343 def added(self):
345 344 """
346 345 Returns list of added ``FileNode`` objects.
347 346 """
348 347 return AddedFileNodesGenerator([n for n in self.status[1]], self)
349 348
350 349 @property
351 350 def changed(self):
352 351 """
353 352 Returns list of modified ``FileNode`` objects.
354 353 """
355 354 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
356 355
357 356 @property
358 357 def removed(self):
359 358 """
360 359 Returns list of removed ``FileNode`` objects.
361 360 """
362 361 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,113 +1,113 b''
1 1 import datetime
2 2 import errno
3 3
4 4 from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset
5 5 from rhodecode.lib.vcs.exceptions import RepositoryError
6 6
7 from ...utils.hgcompat import memfilectx, memctx, hex, tolocal
7 from rhodecode.lib.vcs.utils.hgcompat import memfilectx, memctx, hex, tolocal
8 8
9 9
10 10 class MercurialInMemoryChangeset(BaseInMemoryChangeset):
11 11
12 12 def commit(self, message, author, parents=None, branch=None, date=None,
13 13 **kwargs):
14 14 """
15 15 Performs in-memory commit (doesn't check workdir in any way) and
16 16 returns newly created ``Changeset``. Updates repository's
17 17 ``revisions``.
18 18
19 19 :param message: message of the commit
20 20 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
21 21 :param parents: single parent or sequence of parents from which commit
22 22 would be derieved
23 23 :param date: ``datetime.datetime`` instance. Defaults to
24 24 ``datetime.datetime.now()``.
25 25 :param branch: branch name, as string. If none given, default backend's
26 26 branch would be used.
27 27
28 28 :raises ``CommitError``: if any error occurs while committing
29 29 """
30 30 self.check_integrity(parents)
31 31
32 32 from .repository import MercurialRepository
33 33 if not isinstance(message, unicode) or not isinstance(author, unicode):
34 34 raise RepositoryError('Given message and author needs to be '
35 35 'an <unicode> instance got %r & %r instead'
36 36 % (type(message), type(author)))
37 37
38 38 if branch is None:
39 39 branch = MercurialRepository.DEFAULT_BRANCH_NAME
40 40 kwargs['branch'] = branch
41 41
42 42 def filectxfn(_repo, memctx, path):
43 43 """
44 44 Marks given path as added/changed/removed in a given _repo. This is
45 45 for internal mercurial commit function.
46 46 """
47 47
48 48 # check if this path is removed
49 49 if path in (node.path for node in self.removed):
50 50 # Raising exception is a way to mark node for removal
51 51 raise IOError(errno.ENOENT, '%s is deleted' % path)
52 52
53 53 # check if this path is added
54 54 for node in self.added:
55 55 if node.path == path:
56 56 return memfilectx(path=node.path,
57 57 data=(node.content.encode('utf8')
58 58 if not node.is_binary else node.content),
59 59 islink=False,
60 60 isexec=node.is_executable,
61 61 copied=False)
62 62
63 63 # or changed
64 64 for node in self.changed:
65 65 if node.path == path:
66 66 return memfilectx(path=node.path,
67 67 data=(node.content.encode('utf8')
68 68 if not node.is_binary else node.content),
69 69 islink=False,
70 70 isexec=node.is_executable,
71 71 copied=False)
72 72
73 73 raise RepositoryError("Given path haven't been marked as added,"
74 74 "changed or removed (%s)" % path)
75 75
76 76 parents = [None, None]
77 77 for i, parent in enumerate(self.parents):
78 78 if parent is not None:
79 79 parents[i] = parent._ctx.node()
80 80
81 81 if date and isinstance(date, datetime.datetime):
82 82 date = date.ctime()
83 83
84 84 commit_ctx = memctx(repo=self.repository._repo,
85 85 parents=parents,
86 86 text='',
87 87 files=self.get_paths(),
88 88 filectxfn=filectxfn,
89 89 user=author,
90 90 date=date,
91 91 extra=kwargs)
92 92
93 93 loc = lambda u: tolocal(u.encode('utf-8'))
94 94
95 95 # injecting given _repo params
96 96 commit_ctx._text = loc(message)
97 97 commit_ctx._user = loc(author)
98 98 commit_ctx._date = date
99 99
100 100 # TODO: Catch exceptions!
101 101 n = self.repository._repo.commitctx(commit_ctx)
102 102 # Returns mercurial node
103 103 self._commit_ctx = commit_ctx # For reference
104 104 # Update vcs repository object & recreate mercurial _repo
105 105 # new_ctx = self.repository._repo[node]
106 106 # new_tip = self.repository.get_changeset(new_ctx.hex())
107 107 new_id = hex(n)
108 108 self.repository.revisions.append(new_id)
109 109 self._repo = self.repository._get_repo(create=False)
110 110 self.repository.branches = self.repository._get_branches()
111 111 tip = self.repository.get_changeset()
112 112 self.reset()
113 113 return tip
@@ -1,527 +1,527 b''
1 1 import os
2 2 import time
3 3 import datetime
4 4 import urllib
5 5 import urllib2
6 6
7 7 from rhodecode.lib.vcs.backends.base import BaseRepository
8 8 from .workdir import MercurialWorkdir
9 9 from .changeset import MercurialChangeset
10 10 from .inmemory import MercurialInMemoryChangeset
11 11
12 12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
13 13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
14 14 VCSError, TagAlreadyExistError, TagDoesNotExistError
15 15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
16 16 makedate, safe_unicode
17 17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
19 19 from rhodecode.lib.vcs.utils.paths import abspath
20 20
21 from ...utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
21 from rhodecode.lib.vcs.utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
22 22 get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex
23 23
24 24
25 25 class MercurialRepository(BaseRepository):
26 26 """
27 27 Mercurial repository backend
28 28 """
29 29 DEFAULT_BRANCH_NAME = 'default'
30 30 scm = 'hg'
31 31
32 32 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
33 33 update_after_clone=False):
34 34 """
35 35 Raises RepositoryError if repository could not be find at the given
36 36 ``repo_path``.
37 37
38 38 :param repo_path: local path of the repository
39 39 :param create=False: if set to True, would try to create repository if
40 40 it does not exist rather than raising exception
41 41 :param baseui=None: user data
42 42 :param src_url=None: would try to clone repository from given location
43 43 :param update_after_clone=False: sets update of working copy after
44 44 making a clone
45 45 """
46 46
47 47 if not isinstance(repo_path, str):
48 48 raise VCSError('Mercurial backend requires repository path to '
49 49 'be instance of <str> got %s instead' %
50 50 type(repo_path))
51 51
52 52 self.path = abspath(repo_path)
53 53 self.baseui = baseui or ui.ui()
54 54 # We've set path and ui, now we can set _repo itself
55 55 self._repo = self._get_repo(create, src_url, update_after_clone)
56 56
57 57 @property
58 58 def _empty(self):
59 59 """
60 60 Checks if repository is empty without any changesets
61 61 """
62 62 # TODO: Following raises errors when using InMemoryChangeset...
63 63 # return len(self._repo.changelog) == 0
64 64 return len(self.revisions) == 0
65 65
66 66 @LazyProperty
67 67 def revisions(self):
68 68 """
69 69 Returns list of revisions' ids, in ascending order. Being lazy
70 70 attribute allows external tools to inject shas from cache.
71 71 """
72 72 return self._get_all_revisions()
73 73
74 74 @LazyProperty
75 75 def name(self):
76 76 return os.path.basename(self.path)
77 77
78 78 @LazyProperty
79 79 def branches(self):
80 80 return self._get_branches()
81 81
82 82 def _get_branches(self, closed=False):
83 83 """
84 84 Get's branches for this repository
85 85 Returns only not closed branches by default
86 86
87 87 :param closed: return also closed branches for mercurial
88 88 """
89 89
90 90 if self._empty:
91 91 return {}
92 92
93 93 def _branchtags(localrepo):
94 94 """
95 95 Patched version of mercurial branchtags to not return the closed
96 96 branches
97 97
98 98 :param localrepo: locarepository instance
99 99 """
100 100
101 101 bt = {}
102 102 bt_closed = {}
103 103 for bn, heads in localrepo.branchmap().iteritems():
104 104 tip = heads[-1]
105 105 if 'close' in localrepo.changelog.read(tip)[5]:
106 106 bt_closed[bn] = tip
107 107 else:
108 108 bt[bn] = tip
109 109
110 110 if closed:
111 111 bt.update(bt_closed)
112 112 return bt
113 113
114 114 sortkey = lambda ctx: ctx[0] # sort by name
115 115 _branches = [(safe_unicode(n), hex(h),) for n, h in
116 116 _branchtags(self._repo).items()]
117 117
118 118 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
119 119
120 120 @LazyProperty
121 121 def tags(self):
122 122 """
123 123 Get's tags for this repository
124 124 """
125 125 return self._get_tags()
126 126
127 127 def _get_tags(self):
128 128 if self._empty:
129 129 return {}
130 130
131 131 sortkey = lambda ctx: ctx[0] # sort by name
132 132 _tags = [(safe_unicode(n), hex(h),) for n, h in
133 133 self._repo.tags().items()]
134 134
135 135 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
136 136
137 137 def tag(self, name, user, revision=None, message=None, date=None,
138 138 **kwargs):
139 139 """
140 140 Creates and returns a tag for the given ``revision``.
141 141
142 142 :param name: name for new tag
143 143 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
144 144 :param revision: changeset id for which new tag would be created
145 145 :param message: message of the tag's commit
146 146 :param date: date of tag's commit
147 147
148 148 :raises TagAlreadyExistError: if tag with same name already exists
149 149 """
150 150 if name in self.tags:
151 151 raise TagAlreadyExistError("Tag %s already exists" % name)
152 152 changeset = self.get_changeset(revision)
153 153 local = kwargs.setdefault('local', False)
154 154
155 155 if message is None:
156 156 message = "Added tag %s for changeset %s" % (name,
157 157 changeset.short_id)
158 158
159 159 if date is None:
160 160 date = datetime.datetime.now().ctime()
161 161
162 162 try:
163 163 self._repo.tag(name, changeset._ctx.node(), message, local, user,
164 164 date)
165 165 except Abort, e:
166 166 raise RepositoryError(e.message)
167 167
168 168 # Reinitialize tags
169 169 self.tags = self._get_tags()
170 170 tag_id = self.tags[name]
171 171
172 172 return self.get_changeset(revision=tag_id)
173 173
174 174 def remove_tag(self, name, user, message=None, date=None):
175 175 """
176 176 Removes tag with the given ``name``.
177 177
178 178 :param name: name of the tag to be removed
179 179 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
180 180 :param message: message of the tag's removal commit
181 181 :param date: date of tag's removal commit
182 182
183 183 :raises TagDoesNotExistError: if tag with given name does not exists
184 184 """
185 185 if name not in self.tags:
186 186 raise TagDoesNotExistError("Tag %s does not exist" % name)
187 187 if message is None:
188 188 message = "Removed tag %s" % name
189 189 if date is None:
190 190 date = datetime.datetime.now().ctime()
191 191 local = False
192 192
193 193 try:
194 194 self._repo.tag(name, nullid, message, local, user, date)
195 195 self.tags = self._get_tags()
196 196 except Abort, e:
197 197 raise RepositoryError(e.message)
198 198
199 199 @LazyProperty
200 200 def bookmarks(self):
201 201 """
202 202 Get's bookmarks for this repository
203 203 """
204 204 return self._get_bookmarks()
205 205
206 206 def _get_bookmarks(self):
207 207 if self._empty:
208 208 return {}
209 209
210 210 sortkey = lambda ctx: ctx[0] # sort by name
211 211 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
212 212 self._repo._bookmarks.items()]
213 213 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
214 214
215 215 def _get_all_revisions(self):
216 216
217 217 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
218 218
219 219 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
220 220 context=3):
221 221 """
222 222 Returns (git like) *diff*, as plain text. Shows changes introduced by
223 223 ``rev2`` since ``rev1``.
224 224
225 225 :param rev1: Entry point from which diff is shown. Can be
226 226 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
227 227 the changes since empty state of the repository until ``rev2``
228 228 :param rev2: Until which revision changes should be shown.
229 229 :param ignore_whitespace: If set to ``True``, would not show whitespace
230 230 changes. Defaults to ``False``.
231 231 :param context: How many lines before/after changed lines should be
232 232 shown. Defaults to ``3``.
233 233 """
234 234 if hasattr(rev1, 'raw_id'):
235 235 rev1 = getattr(rev1, 'raw_id')
236 236
237 237 if hasattr(rev2, 'raw_id'):
238 238 rev2 = getattr(rev2, 'raw_id')
239 239
240 240 # Check if given revisions are present at repository (may raise
241 241 # ChangesetDoesNotExistError)
242 242 if rev1 != self.EMPTY_CHANGESET:
243 243 self.get_changeset(rev1)
244 244 self.get_changeset(rev2)
245 245
246 246 file_filter = match(self.path, '', [path])
247 247 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
248 248 opts=diffopts(git=True,
249 249 ignorews=ignore_whitespace,
250 250 context=context)))
251 251
252 252 def _check_url(self, url):
253 253 """
254 254 Function will check given url and try to verify if it's a valid
255 255 link. Sometimes it may happened that mercurial will issue basic
256 256 auth request that can cause whole API to hang when used from python
257 257 or other external calls.
258 258
259 259 On failures it'll raise urllib2.HTTPError, return code 200 if url
260 260 is valid or True if it's a local path
261 261 """
262 262
263 263 from mercurial.util import url as Url
264 264
265 265 # those authnadlers are patched for python 2.6.5 bug an
266 266 # infinit looping when given invalid resources
267 267 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
268 268
269 269 # check first if it's not an local url
270 270 if os.path.isdir(url) or url.startswith('file:'):
271 271 return True
272 272
273 273 handlers = []
274 274 test_uri, authinfo = Url(url).authinfo()
275 275
276 276 if authinfo:
277 277 #create a password manager
278 278 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
279 279 passmgr.add_password(*authinfo)
280 280
281 281 handlers.extend((httpbasicauthhandler(passmgr),
282 282 httpdigestauthhandler(passmgr)))
283 283
284 284 o = urllib2.build_opener(*handlers)
285 285 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
286 286 ('Accept', 'application/mercurial-0.1')]
287 287
288 288 q = {"cmd": 'between'}
289 289 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
290 290 qs = '?%s' % urllib.urlencode(q)
291 291 cu = "%s%s" % (test_uri, qs)
292 292 req = urllib2.Request(cu, None, {})
293 293
294 294 try:
295 295 resp = o.open(req)
296 296 return resp.code == 200
297 297 except Exception, e:
298 298 # means it cannot be cloned
299 299 raise urllib2.URLError(e)
300 300
301 301 def _get_repo(self, create, src_url=None, update_after_clone=False):
302 302 """
303 303 Function will check for mercurial repository in given path and return
304 304 a localrepo object. If there is no repository in that path it will
305 305 raise an exception unless ``create`` parameter is set to True - in
306 306 that case repository would be created and returned.
307 307 If ``src_url`` is given, would try to clone repository from the
308 308 location at given clone_point. Additionally it'll make update to
309 309 working copy accordingly to ``update_after_clone`` flag
310 310 """
311 311 try:
312 312 if src_url:
313 313 url = str(self._get_url(src_url))
314 314 opts = {}
315 315 if not update_after_clone:
316 316 opts.update({'noupdate': True})
317 317 try:
318 318 self._check_url(url)
319 319 clone(self.baseui, url, self.path, **opts)
320 320 # except urllib2.URLError:
321 321 # raise Abort("Got HTTP 404 error")
322 322 except Exception:
323 323 raise
324 324 # Don't try to create if we've already cloned repo
325 325 create = False
326 326 return localrepository(self.baseui, self.path, create=create)
327 327 except (Abort, RepoError), err:
328 328 if create:
329 329 msg = "Cannot create repository at %s. Original error was %s"\
330 330 % (self.path, err)
331 331 else:
332 332 msg = "Not valid repository at %s. Original error was %s"\
333 333 % (self.path, err)
334 334 raise RepositoryError(msg)
335 335
336 336 @LazyProperty
337 337 def in_memory_changeset(self):
338 338 return MercurialInMemoryChangeset(self)
339 339
340 340 @LazyProperty
341 341 def description(self):
342 342 undefined_description = u'unknown'
343 343 return safe_unicode(self._repo.ui.config('web', 'description',
344 344 undefined_description, untrusted=True))
345 345
346 346 @LazyProperty
347 347 def contact(self):
348 348 undefined_contact = u'Unknown'
349 349 return safe_unicode(get_contact(self._repo.ui.config)
350 350 or undefined_contact)
351 351
352 352 @LazyProperty
353 353 def last_change(self):
354 354 """
355 355 Returns last change made on this repository as datetime object
356 356 """
357 357 return date_fromtimestamp(self._get_mtime(), makedate()[1])
358 358
359 359 def _get_mtime(self):
360 360 try:
361 361 return time.mktime(self.get_changeset().date.timetuple())
362 362 except RepositoryError:
363 363 #fallback to filesystem
364 364 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
365 365 st_path = os.path.join(self.path, '.hg', "store")
366 366 if os.path.exists(cl_path):
367 367 return os.stat(cl_path).st_mtime
368 368 else:
369 369 return os.stat(st_path).st_mtime
370 370
371 371 def _get_hidden(self):
372 372 return self._repo.ui.configbool("web", "hidden", untrusted=True)
373 373
374 374 def _get_revision(self, revision):
375 375 """
376 376 Get's an ID revision given as str. This will always return a fill
377 377 40 char revision number
378 378
379 379 :param revision: str or int or None
380 380 """
381 381
382 382 if self._empty:
383 383 raise EmptyRepositoryError("There are no changesets yet")
384 384
385 385 if revision in [-1, 'tip', None]:
386 386 revision = 'tip'
387 387
388 388 try:
389 389 revision = hex(self._repo.lookup(revision))
390 390 except (IndexError, ValueError, RepoLookupError, TypeError):
391 391 raise ChangesetDoesNotExistError("Revision %r does not "
392 392 "exist for this repository %s" \
393 393 % (revision, self))
394 394 return revision
395 395
396 396 def _get_archives(self, archive_name='tip'):
397 397 allowed = self.baseui.configlist("web", "allow_archive",
398 398 untrusted=True)
399 399 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
400 400 if i[0] in allowed or self._repo.ui.configbool("web",
401 401 "allow" + i[0],
402 402 untrusted=True):
403 403 yield {"type": i[0], "extension": i[1], "node": archive_name}
404 404
405 405 def _get_url(self, url):
406 406 """
407 407 Returns normalized url. If schema is not given, would fall
408 408 to filesystem
409 409 (``file:///``) schema.
410 410 """
411 411 url = str(url)
412 412 if url != 'default' and not '://' in url:
413 413 url = "file:" + urllib.pathname2url(url)
414 414 return url
415 415
416 416 def get_changeset(self, revision=None):
417 417 """
418 418 Returns ``MercurialChangeset`` object representing repository's
419 419 changeset at the given ``revision``.
420 420 """
421 421 revision = self._get_revision(revision)
422 422 changeset = MercurialChangeset(repository=self, revision=revision)
423 423 return changeset
424 424
425 425 def get_changesets(self, start=None, end=None, start_date=None,
426 426 end_date=None, branch_name=None, reverse=False):
427 427 """
428 428 Returns iterator of ``MercurialChangeset`` objects from start to end
429 429 (both are inclusive)
430 430
431 431 :param start: None, str, int or mercurial lookup format
432 432 :param end: None, str, int or mercurial lookup format
433 433 :param start_date:
434 434 :param end_date:
435 435 :param branch_name:
436 436 :param reversed: return changesets in reversed order
437 437 """
438 438
439 439 start_raw_id = self._get_revision(start)
440 440 start_pos = self.revisions.index(start_raw_id) if start else None
441 441 end_raw_id = self._get_revision(end)
442 442 end_pos = self.revisions.index(end_raw_id) if end else None
443 443
444 444 if None not in [start, end] and start_pos > end_pos:
445 445 raise RepositoryError("start revision '%s' cannot be "
446 446 "after end revision '%s'" % (start, end))
447 447
448 448 if branch_name and branch_name not in self.branches.keys():
449 449 raise BranchDoesNotExistError('Such branch %s does not exists for'
450 450 ' this repository' % branch_name)
451 451 if end_pos is not None:
452 452 end_pos += 1
453 453
454 454 slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
455 455 self.revisions[start_pos:end_pos]
456 456
457 457 for id_ in slice_:
458 458 cs = self.get_changeset(id_)
459 459 if branch_name and cs.branch != branch_name:
460 460 continue
461 461 if start_date and cs.date < start_date:
462 462 continue
463 463 if end_date and cs.date > end_date:
464 464 continue
465 465
466 466 yield cs
467 467
468 468 def pull(self, url):
469 469 """
470 470 Tries to pull changes from external location.
471 471 """
472 472 url = self._get_url(url)
473 473 try:
474 474 pull(self.baseui, self._repo, url)
475 475 except Abort, err:
476 476 # Propagate error but with vcs's type
477 477 raise RepositoryError(str(err))
478 478
479 479 @LazyProperty
480 480 def workdir(self):
481 481 """
482 482 Returns ``Workdir`` instance for this repository.
483 483 """
484 484 return MercurialWorkdir(self)
485 485
486 486 def get_config_value(self, section, name, config_file=None):
487 487 """
488 488 Returns configuration value for a given [``section``] and ``name``.
489 489
490 490 :param section: Section we want to retrieve value from
491 491 :param name: Name of configuration we want to retrieve
492 492 :param config_file: A path to file which should be used to retrieve
493 493 configuration from (might also be a list of file paths)
494 494 """
495 495 if config_file is None:
496 496 config_file = []
497 497 elif isinstance(config_file, basestring):
498 498 config_file = [config_file]
499 499
500 500 config = self._repo.ui
501 501 for path in config_file:
502 502 config.readconfig(path)
503 503 return config.config(section, name)
504 504
505 505 def get_user_name(self, config_file=None):
506 506 """
507 507 Returns user's name from global configuration file.
508 508
509 509 :param config_file: A path to file which should be used to retrieve
510 510 configuration from (might also be a list of file paths)
511 511 """
512 512 username = self.get_config_value('ui', 'username')
513 513 if username:
514 514 return author_name(username)
515 515 return None
516 516
517 517 def get_user_email(self, config_file=None):
518 518 """
519 519 Returns user's email from global configuration file.
520 520
521 521 :param config_file: A path to file which should be used to retrieve
522 522 configuration from (might also be a list of file paths)
523 523 """
524 524 username = self.get_config_value('ui', 'username')
525 525 if username:
526 526 return author_email(username)
527 527 return None
@@ -1,21 +1,21 b''
1 1 from rhodecode.lib.vcs.backends.base import BaseWorkdir
2 2 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
3 3
4 from ...utils.hgcompat import hg_merge
4 from rhodecode.lib.vcs.utils.hgcompat import hg_merge
5 5
6 6
7 7 class MercurialWorkdir(BaseWorkdir):
8 8
9 9 def get_branch(self):
10 10 return self.repository._repo.dirstate.branch()
11 11
12 12 def get_changeset(self):
13 13 return self.repository.get_changeset()
14 14
15 15 def checkout_branch(self, branch=None):
16 16 if branch is None:
17 17 branch = self.repository.DEFAULT_BRANCH_NAME
18 18 if branch not in self.repository.branches:
19 19 raise BranchDoesNotExistError
20 20
21 21 hg_merge.update(self.repository._repo, branch, False, False, None)
@@ -1,613 +1,613 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.nodes
4 4 ~~~~~~~~~
5 5
6 6 Module holding everything related to vcs nodes.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11 import os
12 12 import stat
13 13 import posixpath
14 14 import mimetypes
15 15
16 16 from pygments import lexers
17 17
18 18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
19 from rhodecode.lib.vcs.utils import safe_unicode
20 20 from rhodecode.lib.vcs.exceptions import NodeError
21 21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
22 22 from rhodecode.lib.vcs.backends.base import EmptyChangeset
23 23
24 24
25 25 class NodeKind:
26 26 SUBMODULE = -1
27 27 DIR = 1
28 28 FILE = 2
29 29
30 30
31 31 class NodeState:
32 32 ADDED = u'added'
33 33 CHANGED = u'changed'
34 34 NOT_CHANGED = u'not changed'
35 35 REMOVED = u'removed'
36 36
37 37
38 38 class NodeGeneratorBase(object):
39 39 """
40 40 Base class for removed added and changed filenodes, it's a lazy generator
41 41 class that will create filenodes only on iteration or call
42 42
43 43 The len method doesn't need to create filenodes at all
44 44 """
45 45
46 46 def __init__(self, current_paths, cs):
47 47 self.cs = cs
48 48 self.current_paths = current_paths
49 49
50 50 def __call__(self):
51 51 return [n for n in self]
52 52
53 53 def __getslice__(self, i, j):
54 54 for p in self.current_paths[i:j]:
55 55 yield self.cs.get_node(p)
56 56
57 57 def __len__(self):
58 58 return len(self.current_paths)
59 59
60 60 def __iter__(self):
61 61 for p in self.current_paths:
62 62 yield self.cs.get_node(p)
63 63
64 64
65 65 class AddedFileNodesGenerator(NodeGeneratorBase):
66 66 """
67 67 Class holding Added files for current changeset
68 68 """
69 69 pass
70 70
71 71
72 72 class ChangedFileNodesGenerator(NodeGeneratorBase):
73 73 """
74 74 Class holding Changed files for current changeset
75 75 """
76 76 pass
77 77
78 78
79 79 class RemovedFileNodesGenerator(NodeGeneratorBase):
80 80 """
81 81 Class holding removed files for current changeset
82 82 """
83 83 def __iter__(self):
84 84 for p in self.current_paths:
85 85 yield RemovedFileNode(path=p)
86 86
87 87 def __getslice__(self, i, j):
88 88 for p in self.current_paths[i:j]:
89 89 yield RemovedFileNode(path=p)
90 90
91 91
92 92 class Node(object):
93 93 """
94 94 Simplest class representing file or directory on repository. SCM backends
95 95 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
96 96 directly.
97 97
98 98 Node's ``path`` cannot start with slash as we operate on *relative* paths
99 99 only. Moreover, every single node is identified by the ``path`` attribute,
100 100 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
101 101 """
102 102
103 103 def __init__(self, path, kind):
104 104 if path.startswith('/'):
105 105 raise NodeError("Cannot initialize Node objects with slash at "
106 106 "the beginning as only relative paths are supported")
107 107 self.path = path.rstrip('/')
108 108 if path == '' and kind != NodeKind.DIR:
109 109 raise NodeError("Only DirNode and its subclasses may be "
110 110 "initialized with empty path")
111 111 self.kind = kind
112 112 #self.dirs, self.files = [], []
113 113 if self.is_root() and not self.is_dir():
114 114 raise NodeError("Root node cannot be FILE kind")
115 115
116 116 @LazyProperty
117 117 def parent(self):
118 118 parent_path = self.get_parent_path()
119 119 if parent_path:
120 120 if self.changeset:
121 121 return self.changeset.get_node(parent_path)
122 122 return DirNode(parent_path)
123 123 return None
124 124
125 125 @LazyProperty
126 126 def unicode_path(self):
127 127 return safe_unicode(self.path)
128 128
129 129 @LazyProperty
130 130 def name(self):
131 131 """
132 132 Returns name of the node so if its path
133 133 then only last part is returned.
134 134 """
135 135 return safe_unicode(self.path.rstrip('/').split('/')[-1])
136 136
137 137 def _get_kind(self):
138 138 return self._kind
139 139
140 140 def _set_kind(self, kind):
141 141 if hasattr(self, '_kind'):
142 142 raise NodeError("Cannot change node's kind")
143 143 else:
144 144 self._kind = kind
145 145 # Post setter check (path's trailing slash)
146 146 if self.path.endswith('/'):
147 147 raise NodeError("Node's path cannot end with slash")
148 148
149 149 kind = property(_get_kind, _set_kind)
150 150
151 151 def __cmp__(self, other):
152 152 """
153 153 Comparator using name of the node, needed for quick list sorting.
154 154 """
155 155 kind_cmp = cmp(self.kind, other.kind)
156 156 if kind_cmp:
157 157 return kind_cmp
158 158 return cmp(self.name, other.name)
159 159
160 160 def __eq__(self, other):
161 161 for attr in ['name', 'path', 'kind']:
162 162 if getattr(self, attr) != getattr(other, attr):
163 163 return False
164 164 if self.is_file():
165 165 if self.content != other.content:
166 166 return False
167 167 else:
168 168 # For DirNode's check without entering each dir
169 169 self_nodes_paths = list(sorted(n.path for n in self.nodes))
170 170 other_nodes_paths = list(sorted(n.path for n in self.nodes))
171 171 if self_nodes_paths != other_nodes_paths:
172 172 return False
173 173 return True
174 174
175 175 def __nq__(self, other):
176 176 return not self.__eq__(other)
177 177
178 178 def __repr__(self):
179 179 return '<%s %r>' % (self.__class__.__name__, self.path)
180 180
181 181 def __str__(self):
182 182 return self.__repr__()
183 183
184 184 def __unicode__(self):
185 185 return self.name
186 186
187 187 def get_parent_path(self):
188 188 """
189 189 Returns node's parent path or empty string if node is root.
190 190 """
191 191 if self.is_root():
192 192 return ''
193 193 return posixpath.dirname(self.path.rstrip('/')) + '/'
194 194
195 195 def is_file(self):
196 196 """
197 197 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
198 198 otherwise.
199 199 """
200 200 return self.kind == NodeKind.FILE
201 201
202 202 def is_dir(self):
203 203 """
204 204 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
205 205 otherwise.
206 206 """
207 207 return self.kind == NodeKind.DIR
208 208
209 209 def is_root(self):
210 210 """
211 211 Returns ``True`` if node is a root node and ``False`` otherwise.
212 212 """
213 213 return self.kind == NodeKind.DIR and self.path == ''
214 214
215 215 def is_submodule(self):
216 216 """
217 217 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
218 218 otherwise.
219 219 """
220 220 return self.kind == NodeKind.SUBMODULE
221 221
222 222 @LazyProperty
223 223 def added(self):
224 224 return self.state is NodeState.ADDED
225 225
226 226 @LazyProperty
227 227 def changed(self):
228 228 return self.state is NodeState.CHANGED
229 229
230 230 @LazyProperty
231 231 def not_changed(self):
232 232 return self.state is NodeState.NOT_CHANGED
233 233
234 234 @LazyProperty
235 235 def removed(self):
236 236 return self.state is NodeState.REMOVED
237 237
238 238
239 239 class FileNode(Node):
240 240 """
241 241 Class representing file nodes.
242 242
243 243 :attribute: path: path to the node, relative to repostiory's root
244 244 :attribute: content: if given arbitrary sets content of the file
245 245 :attribute: changeset: if given, first time content is accessed, callback
246 246 :attribute: mode: octal stat mode for a node. Default is 0100644.
247 247 """
248 248
249 249 def __init__(self, path, content=None, changeset=None, mode=None):
250 250 """
251 251 Only one of ``content`` and ``changeset`` may be given. Passing both
252 252 would raise ``NodeError`` exception.
253 253
254 254 :param path: relative path to the node
255 255 :param content: content may be passed to constructor
256 256 :param changeset: if given, will use it to lazily fetch content
257 257 :param mode: octal representation of ST_MODE (i.e. 0100644)
258 258 """
259 259
260 260 if content and changeset:
261 261 raise NodeError("Cannot use both content and changeset")
262 262 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
263 263 self.changeset = changeset
264 264 self._content = content
265 265 self._mode = mode or 0100644
266 266
267 267 @LazyProperty
268 268 def mode(self):
269 269 """
270 270 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
271 271 use value given at initialization or 0100644 (default).
272 272 """
273 273 if self.changeset:
274 274 mode = self.changeset.get_file_mode(self.path)
275 275 else:
276 276 mode = self._mode
277 277 return mode
278 278
279 279 @property
280 280 def content(self):
281 281 """
282 282 Returns lazily content of the FileNode. If possible, would try to
283 283 decode content from UTF-8.
284 284 """
285 285 if self.changeset:
286 286 content = self.changeset.get_file_content(self.path)
287 287 else:
288 288 content = self._content
289 289
290 290 if bool(content and '\0' in content):
291 291 return content
292 292 return safe_unicode(content)
293 293
294 294 @LazyProperty
295 295 def size(self):
296 296 if self.changeset:
297 297 return self.changeset.get_file_size(self.path)
298 298 raise NodeError("Cannot retrieve size of the file without related "
299 299 "changeset attribute")
300 300
301 301 @LazyProperty
302 302 def message(self):
303 303 if self.changeset:
304 304 return self.last_changeset.message
305 305 raise NodeError("Cannot retrieve message of the file without related "
306 306 "changeset attribute")
307 307
308 308 @LazyProperty
309 309 def last_changeset(self):
310 310 if self.changeset:
311 311 return self.changeset.get_file_changeset(self.path)
312 312 raise NodeError("Cannot retrieve last changeset of the file without "
313 313 "related changeset attribute")
314 314
315 315 def get_mimetype(self):
316 316 """
317 317 Mimetype is calculated based on the file's content. If ``_mimetype``
318 318 attribute is available, it will be returned (backends which store
319 319 mimetypes or can easily recognize them, should set this private
320 320 attribute to indicate that type should *NOT* be calculated).
321 321 """
322 322 if hasattr(self, '_mimetype'):
323 323 if (isinstance(self._mimetype, (tuple, list,)) and
324 324 len(self._mimetype) == 2):
325 325 return self._mimetype
326 326 else:
327 327 raise NodeError('given _mimetype attribute must be an 2 '
328 328 'element list or tuple')
329 329
330 330 mtype, encoding = mimetypes.guess_type(self.name)
331 331
332 332 if mtype is None:
333 333 if self.is_binary:
334 334 mtype = 'application/octet-stream'
335 335 encoding = None
336 336 else:
337 337 mtype = 'text/plain'
338 338 encoding = None
339 339 return mtype, encoding
340 340
341 341 @LazyProperty
342 342 def mimetype(self):
343 343 """
344 344 Wrapper around full mimetype info. It returns only type of fetched
345 345 mimetype without the encoding part. use get_mimetype function to fetch
346 346 full set of (type,encoding)
347 347 """
348 348 return self.get_mimetype()[0]
349 349
350 350 @LazyProperty
351 351 def mimetype_main(self):
352 352 return self.mimetype.split('/')[0]
353 353
354 354 @LazyProperty
355 355 def lexer(self):
356 356 """
357 357 Returns pygment's lexer class. Would try to guess lexer taking file's
358 358 content, name and mimetype.
359 359 """
360 360 try:
361 361 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
362 362 except lexers.ClassNotFound:
363 363 lexer = lexers.TextLexer()
364 364 # returns first alias
365 365 return lexer
366 366
367 367 @LazyProperty
368 368 def lexer_alias(self):
369 369 """
370 370 Returns first alias of the lexer guessed for this file.
371 371 """
372 372 return self.lexer.aliases[0]
373 373
374 374 @LazyProperty
375 375 def history(self):
376 376 """
377 377 Returns a list of changeset for this file in which the file was changed
378 378 """
379 379 if self.changeset is None:
380 380 raise NodeError('Unable to get changeset for this FileNode')
381 381 return self.changeset.get_file_history(self.path)
382 382
383 383 @LazyProperty
384 384 def annotate(self):
385 385 """
386 386 Returns a list of three element tuples with lineno,changeset and line
387 387 """
388 388 if self.changeset is None:
389 389 raise NodeError('Unable to get changeset for this FileNode')
390 390 return self.changeset.get_file_annotate(self.path)
391 391
392 392 @LazyProperty
393 393 def state(self):
394 394 if not self.changeset:
395 395 raise NodeError("Cannot check state of the node if it's not "
396 396 "linked with changeset")
397 397 elif self.path in (node.path for node in self.changeset.added):
398 398 return NodeState.ADDED
399 399 elif self.path in (node.path for node in self.changeset.changed):
400 400 return NodeState.CHANGED
401 401 else:
402 402 return NodeState.NOT_CHANGED
403 403
404 404 @property
405 405 def is_binary(self):
406 406 """
407 407 Returns True if file has binary content.
408 408 """
409 409 _bin = '\0' in self.content
410 410 return _bin
411 411
412 412 @LazyProperty
413 413 def extension(self):
414 414 """Returns filenode extension"""
415 415 return self.name.split('.')[-1]
416 416
417 417 def is_executable(self):
418 418 """
419 419 Returns ``True`` if file has executable flag turned on.
420 420 """
421 421 return bool(self.mode & stat.S_IXUSR)
422 422
423 423 def __repr__(self):
424 424 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
425 425 getattr(self.changeset, 'short_id', ''))
426 426
427 427
428 428 class RemovedFileNode(FileNode):
429 429 """
430 430 Dummy FileNode class - trying to access any public attribute except path,
431 431 name, kind or state (or methods/attributes checking those two) would raise
432 432 RemovedFileNodeError.
433 433 """
434 434 ALLOWED_ATTRIBUTES = [
435 435 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
436 436 'added', 'changed', 'not_changed', 'removed'
437 437 ]
438 438
439 439 def __init__(self, path):
440 440 """
441 441 :param path: relative path to the node
442 442 """
443 443 super(RemovedFileNode, self).__init__(path=path)
444 444
445 445 def __getattribute__(self, attr):
446 446 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
447 447 return super(RemovedFileNode, self).__getattribute__(attr)
448 448 raise RemovedFileNodeError("Cannot access attribute %s on "
449 449 "RemovedFileNode" % attr)
450 450
451 451 @LazyProperty
452 452 def state(self):
453 453 return NodeState.REMOVED
454 454
455 455
456 456 class DirNode(Node):
457 457 """
458 458 DirNode stores list of files and directories within this node.
459 459 Nodes may be used standalone but within repository context they
460 460 lazily fetch data within same repositorty's changeset.
461 461 """
462 462
463 463 def __init__(self, path, nodes=(), changeset=None):
464 464 """
465 465 Only one of ``nodes`` and ``changeset`` may be given. Passing both
466 466 would raise ``NodeError`` exception.
467 467
468 468 :param path: relative path to the node
469 469 :param nodes: content may be passed to constructor
470 470 :param changeset: if given, will use it to lazily fetch content
471 471 :param size: always 0 for ``DirNode``
472 472 """
473 473 if nodes and changeset:
474 474 raise NodeError("Cannot use both nodes and changeset")
475 475 super(DirNode, self).__init__(path, NodeKind.DIR)
476 476 self.changeset = changeset
477 477 self._nodes = nodes
478 478
479 479 @LazyProperty
480 480 def content(self):
481 481 raise NodeError("%s represents a dir and has no ``content`` attribute"
482 482 % self)
483 483
484 484 @LazyProperty
485 485 def nodes(self):
486 486 if self.changeset:
487 487 nodes = self.changeset.get_nodes(self.path)
488 488 else:
489 489 nodes = self._nodes
490 490 self._nodes_dict = dict((node.path, node) for node in nodes)
491 491 return sorted(nodes)
492 492
493 493 @LazyProperty
494 494 def files(self):
495 495 return sorted((node for node in self.nodes if node.is_file()))
496 496
497 497 @LazyProperty
498 498 def dirs(self):
499 499 return sorted((node for node in self.nodes if node.is_dir()))
500 500
501 501 def __iter__(self):
502 502 for node in self.nodes:
503 503 yield node
504 504
505 505 def get_node(self, path):
506 506 """
507 507 Returns node from within this particular ``DirNode``, so it is now
508 508 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
509 509 'docs'. In order to access deeper nodes one must fetch nodes between
510 510 them first - this would work::
511 511
512 512 docs = root.get_node('docs')
513 513 docs.get_node('api').get_node('index.rst')
514 514
515 515 :param: path - relative to the current node
516 516
517 517 .. note::
518 518 To access lazily (as in example above) node have to be initialized
519 519 with related changeset object - without it node is out of
520 520 context and may know nothing about anything else than nearest
521 521 (located at same level) nodes.
522 522 """
523 523 try:
524 524 path = path.rstrip('/')
525 525 if path == '':
526 526 raise NodeError("Cannot retrieve node without path")
527 527 self.nodes # access nodes first in order to set _nodes_dict
528 528 paths = path.split('/')
529 529 if len(paths) == 1:
530 530 if not self.is_root():
531 531 path = '/'.join((self.path, paths[0]))
532 532 else:
533 533 path = paths[0]
534 534 return self._nodes_dict[path]
535 535 elif len(paths) > 1:
536 536 if self.changeset is None:
537 537 raise NodeError("Cannot access deeper "
538 538 "nodes without changeset")
539 539 else:
540 540 path1, path2 = paths[0], '/'.join(paths[1:])
541 541 return self.get_node(path1).get_node(path2)
542 542 else:
543 543 raise KeyError
544 544 except KeyError:
545 545 raise NodeError("Node does not exist at %s" % path)
546 546
547 547 @LazyProperty
548 548 def state(self):
549 549 raise NodeError("Cannot access state of DirNode")
550 550
551 551 @LazyProperty
552 552 def size(self):
553 553 size = 0
554 554 for root, dirs, files in self.changeset.walk(self.path):
555 555 for f in files:
556 556 size += f.size
557 557
558 558 return size
559 559
560 560 def __repr__(self):
561 561 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
562 562 getattr(self.changeset, 'short_id', ''))
563 563
564 564
565 565 class RootNode(DirNode):
566 566 """
567 567 DirNode being the root node of the repository.
568 568 """
569 569
570 570 def __init__(self, nodes=(), changeset=None):
571 571 super(RootNode, self).__init__(path='', nodes=nodes,
572 572 changeset=changeset)
573 573
574 574 def __repr__(self):
575 575 return '<%s>' % self.__class__.__name__
576 576
577 577
578 578 class SubModuleNode(Node):
579 579 """
580 580 represents a SubModule of Git or SubRepo of Mercurial
581 581 """
582 582 is_binary = False
583 583 size = 0
584 584
585 585 def __init__(self, name, url=None, changeset=None, alias=None):
586 586 self.path = name
587 587 self.kind = NodeKind.SUBMODULE
588 588 self.alias = alias
589 589 # we have to use emptyChangeset here since this can point to svn/git/hg
590 590 # submodules we cannot get from repository
591 591 self.changeset = EmptyChangeset(str(changeset), alias=alias)
592 592 self.url = url or self._extract_submodule_url()
593 593
594 594 def __repr__(self):
595 595 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
596 596 getattr(self.changeset, 'short_id', ''))
597 597
598 598 def _extract_submodule_url(self):
599 599 if self.alias == 'git':
600 600 #TODO: find a way to parse gits submodule file and extract the
601 601 # linking URL
602 602 return self.path
603 603 if self.alias == 'hg':
604 604 return self.path
605 605
606 606 @LazyProperty
607 607 def name(self):
608 608 """
609 609 Returns name of the node so if its path
610 610 then only last part is returned.
611 611 """
612 612 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
613 613 return u'%s @ %s' % (org, self.changeset.short_id)
@@ -1,336 +1,344 b''
1 1 from __future__ import with_statement
2 2
3 3 from rhodecode.lib import vcs
4 4 import datetime
5 5 from base import BackendTestMixin
6 6 from conf import SCM_TESTS
7 7 from rhodecode.lib.vcs.backends.base import BaseChangeset
8 8 from rhodecode.lib.vcs.nodes import FileNode
9 9 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
10 10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 11 from rhodecode.lib.vcs.exceptions import RepositoryError
12 12 from rhodecode.lib.vcs.utils.compat import unittest
13 13
14 14
15 15 class TestBaseChangeset(unittest.TestCase):
16 16
17 17 def test_as_dict(self):
18 18 changeset = BaseChangeset()
19 19 changeset.id = 'ID'
20 20 changeset.raw_id = 'RAW_ID'
21 21 changeset.short_id = 'SHORT_ID'
22 22 changeset.revision = 1009
23 23 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
24 24 changeset.message = 'Message of a commit'
25 25 changeset.author = 'Joe Doe <joe.doe@example.com>'
26 26 changeset.added = [FileNode('foo/bar/baz'), FileNode('foobar')]
27 27 changeset.changed = []
28 28 changeset.removed = []
29 29 self.assertEqual(changeset.as_dict(), {
30 30 'id': 'ID',
31 31 'raw_id': 'RAW_ID',
32 32 'short_id': 'SHORT_ID',
33 33 'revision': 1009,
34 34 'date': datetime.datetime(2011, 1, 30, 1, 45),
35 35 'message': 'Message of a commit',
36 36 'author': {
37 37 'name': 'Joe Doe',
38 38 'email': 'joe.doe@example.com',
39 39 },
40 40 'added': ['foo/bar/baz', 'foobar'],
41 41 'changed': [],
42 42 'removed': [],
43 43 })
44 44
45 45 class ChangesetsWithCommitsTestCaseixin(BackendTestMixin):
46 46 recreate_repo_per_test = True
47 47
48 48 @classmethod
49 49 def _get_commits(cls):
50 50 start_date = datetime.datetime(2010, 1, 1, 20)
51 51 for x in xrange(5):
52 52 yield {
53 53 'message': 'Commit %d' % x,
54 54 'author': 'Joe Doe <joe.doe@example.com>',
55 55 'date': start_date + datetime.timedelta(hours=12 * x),
56 56 'added': [
57 57 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
58 58 ],
59 59 }
60 60
61 61 def test_new_branch(self):
62 62 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
63 63 content='Documentation\n'))
64 64 foobar_tip = self.imc.commit(
65 65 message=u'New branch: foobar',
66 66 author=u'joe',
67 67 branch='foobar',
68 68 )
69 69 self.assertTrue('foobar' in self.repo.branches)
70 70 self.assertEqual(foobar_tip.branch, 'foobar')
71 71 # 'foobar' should be the only branch that contains the new commit
72 72 self.assertNotEqual(*self.repo.branches.values())
73 73
74 74 def test_new_head_in_default_branch(self):
75 75 tip = self.repo.get_changeset()
76 76 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
77 77 content='Documentation\n'))
78 78 foobar_tip = self.imc.commit(
79 79 message=u'New branch: foobar',
80 80 author=u'joe',
81 81 branch='foobar',
82 82 parents=[tip],
83 83 )
84 84 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
85 85 content='Documentation\nand more...\n'))
86 86 newtip = self.imc.commit(
87 87 message=u'At default branch',
88 88 author=u'joe',
89 89 branch=foobar_tip.branch,
90 90 parents=[foobar_tip],
91 91 )
92 92
93 93 newest_tip = self.imc.commit(
94 94 message=u'Merged with %s' % foobar_tip.raw_id,
95 95 author=u'joe',
96 96 branch=self.backend_class.DEFAULT_BRANCH_NAME,
97 97 parents=[newtip, foobar_tip],
98 98 )
99 99
100 100 self.assertEqual(newest_tip.branch,
101 101 self.backend_class.DEFAULT_BRANCH_NAME)
102 102
103 103 def test_get_changesets_respects_branch_name(self):
104 104 tip = self.repo.get_changeset()
105 105 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
106 106 content='Documentation\n'))
107 107 doc_changeset = self.imc.commit(
108 108 message=u'New branch: docs',
109 109 author=u'joe',
110 110 branch='docs',
111 111 )
112 112 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
113 113 self.imc.commit(
114 114 message=u'Back in default branch',
115 115 author=u'joe',
116 116 parents=[tip],
117 117 )
118 118 default_branch_changesets = self.repo.get_changesets(
119 119 branch_name=self.repo.DEFAULT_BRANCH_NAME)
120 120 self.assertNotIn(doc_changeset, default_branch_changesets)
121 121
122 def test_get_changeset_by_branch(self):
123 for branch, sha in self.repo.branches.iteritems():
124 self.assertEqual(sha, self.repo.get_changeset(branch).raw_id)
125
126 def test_get_changeset_by_tag(self):
127 for tag, sha in self.repo.tags.iteritems():
128 self.assertEqual(sha, self.repo.get_changeset(tag).raw_id)
129
122 130
123 131 class ChangesetsTestCaseMixin(BackendTestMixin):
124 132 recreate_repo_per_test = False
125 133
126 134 @classmethod
127 135 def _get_commits(cls):
128 136 start_date = datetime.datetime(2010, 1, 1, 20)
129 137 for x in xrange(5):
130 138 yield {
131 139 'message': u'Commit %d' % x,
132 140 'author': u'Joe Doe <joe.doe@example.com>',
133 141 'date': start_date + datetime.timedelta(hours=12 * x),
134 142 'added': [
135 143 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
136 144 ],
137 145 }
138 146
139 147 def test_simple(self):
140 148 tip = self.repo.get_changeset()
141 149 self.assertEqual(tip.date, datetime.datetime(2010, 1, 3, 20))
142 150
143 151 def test_get_changesets_is_ordered_by_date(self):
144 152 changesets = list(self.repo.get_changesets())
145 153 ordered_by_date = sorted(changesets,
146 154 key=lambda cs: cs.date)
147 155 self.assertItemsEqual(changesets, ordered_by_date)
148 156
149 157 def test_get_changesets_respects_start(self):
150 158 second_id = self.repo.revisions[1]
151 159 changesets = list(self.repo.get_changesets(start=second_id))
152 160 self.assertEqual(len(changesets), 4)
153 161
154 162 def test_get_changesets_numerical_id_respects_start(self):
155 163 second_id = 1
156 164 changesets = list(self.repo.get_changesets(start=second_id))
157 165 self.assertEqual(len(changesets), 4)
158 166
159 167 def test_get_changesets_includes_start_changeset(self):
160 168 second_id = self.repo.revisions[1]
161 169 changesets = list(self.repo.get_changesets(start=second_id))
162 170 self.assertEqual(changesets[0].raw_id, second_id)
163 171
164 172 def test_get_changesets_respects_end(self):
165 173 second_id = self.repo.revisions[1]
166 174 changesets = list(self.repo.get_changesets(end=second_id))
167 175 self.assertEqual(changesets[-1].raw_id, second_id)
168 176 self.assertEqual(len(changesets), 2)
169 177
170 178 def test_get_changesets_numerical_id_respects_end(self):
171 179 second_id = 1
172 180 changesets = list(self.repo.get_changesets(end=second_id))
173 181 self.assertEqual(changesets.index(changesets[-1]), second_id)
174 182 self.assertEqual(len(changesets), 2)
175 183
176 184 def test_get_changesets_respects_both_start_and_end(self):
177 185 second_id = self.repo.revisions[1]
178 186 third_id = self.repo.revisions[2]
179 187 changesets = list(self.repo.get_changesets(start=second_id,
180 188 end=third_id))
181 189 self.assertEqual(len(changesets), 2)
182 190
183 191 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
184 192 changesets = list(self.repo.get_changesets(start=2, end=3))
185 193 self.assertEqual(len(changesets), 2)
186 194
187 195 def test_get_changesets_includes_end_changeset(self):
188 196 second_id = self.repo.revisions[1]
189 197 changesets = list(self.repo.get_changesets(end=second_id))
190 198 self.assertEqual(changesets[-1].raw_id, second_id)
191 199
192 200 def test_get_changesets_respects_start_date(self):
193 201 start_date = datetime.datetime(2010, 2, 1)
194 202 for cs in self.repo.get_changesets(start_date=start_date):
195 203 self.assertGreaterEqual(cs.date, start_date)
196 204
197 205 def test_get_changesets_respects_end_date(self):
198 206 end_date = datetime.datetime(2010, 2, 1)
199 207 for cs in self.repo.get_changesets(end_date=end_date):
200 208 self.assertLessEqual(cs.date, end_date)
201 209
202 210 def test_get_changesets_respects_reverse(self):
203 211 changesets_id_list = [cs.raw_id for cs in
204 212 self.repo.get_changesets(reverse=True)]
205 213 self.assertItemsEqual(changesets_id_list, reversed(self.repo.revisions))
206 214
207 215 def test_get_filenodes_generator(self):
208 216 tip = self.repo.get_changeset()
209 217 filepaths = [node.path for node in tip.get_filenodes_generator()]
210 218 self.assertItemsEqual(filepaths, ['file_%d.txt' % x for x in xrange(5)])
211 219
212 220 def test_size(self):
213 221 tip = self.repo.get_changeset()
214 222 size = 5 * len('Foobar N') # Size of 5 files
215 223 self.assertEqual(tip.size, size)
216 224
217 225 def test_author(self):
218 226 tip = self.repo.get_changeset()
219 227 self.assertEqual(tip.author, u'Joe Doe <joe.doe@example.com>')
220 228
221 229 def test_author_name(self):
222 230 tip = self.repo.get_changeset()
223 231 self.assertEqual(tip.author_name, u'Joe Doe')
224 232
225 233 def test_author_email(self):
226 234 tip = self.repo.get_changeset()
227 235 self.assertEqual(tip.author_email, u'joe.doe@example.com')
228 236
229 237 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
230 238 with self.assertRaises(ChangesetDoesNotExistError):
231 239 list(self.repo.get_changesets(start='foobar'))
232 240
233 241 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
234 242 with self.assertRaises(ChangesetDoesNotExistError):
235 243 list(self.repo.get_changesets(end='foobar'))
236 244
237 245 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
238 246 with self.assertRaises(BranchDoesNotExistError):
239 247 list(self.repo.get_changesets(branch_name='foobar'))
240 248
241 249 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
242 250 start = self.repo.revisions[-1]
243 251 end = self.repo.revisions[0]
244 252 with self.assertRaises(RepositoryError):
245 253 list(self.repo.get_changesets(start=start, end=end))
246 254
247 255 def test_get_changesets_numerical_id_reversed(self):
248 256 with self.assertRaises(RepositoryError):
249 257 [x for x in self.repo.get_changesets(start=3, end=2)]
250 258
251 259 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
252 260 with self.assertRaises(RepositoryError):
253 261 last = len(self.repo.revisions)
254 262 list(self.repo.get_changesets(start=last-1, end=last-2))
255 263
256 264 def test_get_changesets_numerical_id_last_zero_error(self):
257 265 with self.assertRaises(RepositoryError):
258 266 last = len(self.repo.revisions)
259 267 list(self.repo.get_changesets(start=last-1, end=0))
260 268
261 269
262 270 class ChangesetsChangesTestCaseMixin(BackendTestMixin):
263 271 recreate_repo_per_test = False
264 272
265 273 @classmethod
266 274 def _get_commits(cls):
267 275 return [
268 276 {
269 277 'message': u'Initial',
270 278 'author': u'Joe Doe <joe.doe@example.com>',
271 279 'date': datetime.datetime(2010, 1, 1, 20),
272 280 'added': [
273 281 FileNode('foo/bar', content='foo'),
274 282 FileNode('foobar', content='foo'),
275 283 FileNode('qwe', content='foo'),
276 284 ],
277 285 },
278 286 {
279 287 'message': u'Massive changes',
280 288 'author': u'Joe Doe <joe.doe@example.com>',
281 289 'date': datetime.datetime(2010, 1, 1, 22),
282 290 'added': [FileNode('fallout', content='War never changes')],
283 291 'changed': [
284 292 FileNode('foo/bar', content='baz'),
285 293 FileNode('foobar', content='baz'),
286 294 ],
287 295 'removed': [FileNode('qwe')],
288 296 },
289 297 ]
290 298
291 299 def test_initial_commit(self):
292 300 changeset = self.repo.get_changeset(0)
293 301 self.assertItemsEqual(changeset.added, [
294 302 changeset.get_node('foo/bar'),
295 303 changeset.get_node('foobar'),
296 304 changeset.get_node('qwe'),
297 305 ])
298 306 self.assertItemsEqual(changeset.changed, [])
299 307 self.assertItemsEqual(changeset.removed, [])
300 308
301 309 def test_head_added(self):
302 310 changeset = self.repo.get_changeset()
303 311 self.assertItemsEqual(changeset.added, [
304 312 changeset.get_node('fallout'),
305 313 ])
306 314 self.assertItemsEqual(changeset.changed, [
307 315 changeset.get_node('foo/bar'),
308 316 changeset.get_node('foobar'),
309 317 ])
310 318 self.assertEqual(len(changeset.removed), 1)
311 319 self.assertEqual(list(changeset.removed)[0].path, 'qwe')
312 320
313 321
314 322 # For each backend create test case class
315 323 for alias in SCM_TESTS:
316 324 attrs = {
317 325 'backend_alias': alias,
318 326 }
319 327 # tests with additional commits
320 328 cls_name = ''.join(('%s changesets with commits test' % alias).title().split())
321 329 bases = (ChangesetsWithCommitsTestCaseixin, unittest.TestCase)
322 330 globals()[cls_name] = type(cls_name, bases, attrs)
323 331
324 332 # tests without additional commits
325 333 cls_name = ''.join(('%s changesets test' % alias).title().split())
326 334 bases = (ChangesetsTestCaseMixin, unittest.TestCase)
327 335 globals()[cls_name] = type(cls_name, bases, attrs)
328 336
329 337 # tests changes
330 338 cls_name = ''.join(('%s changesets changes test' % alias).title().split())
331 339 bases = (ChangesetsChangesTestCaseMixin, unittest.TestCase)
332 340 globals()[cls_name] = type(cls_name, bases, attrs)
333 341
334 342
335 343 if __name__ == '__main__':
336 344 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now