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