##// END OF EJS Templates
fixes #630 git statistics do too much work making them slow....
marcink -
r2968:4abfb1af beta
parent child Browse files
Show More
@@ -1,489 +1,493 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, EmptyChangeset
12 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
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, ChangedFileNodesGenerator,\
15 AddedFileNodesGenerator, RemovedFileNodesGenerator
15 from rhodecode.lib.vcs.utils import safe_unicode
16 from rhodecode.lib.vcs.utils import safe_unicode
16 from rhodecode.lib.vcs.utils import date_fromtimestamp
17 from rhodecode.lib.vcs.utils import date_fromtimestamp
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18
19
19
20
20 class GitChangeset(BaseChangeset):
21 class GitChangeset(BaseChangeset):
21 """
22 """
22 Represents state of the repository at single revision.
23 Represents state of the repository at single revision.
23 """
24 """
24
25
25 def __init__(self, repository, revision):
26 def __init__(self, repository, revision):
26 self._stat_modes = {}
27 self._stat_modes = {}
27 self.repository = repository
28 self.repository = repository
28
29
29 try:
30 try:
30 commit = self.repository._repo.get_object(revision)
31 commit = self.repository._repo.get_object(revision)
31 if isinstance(commit, objects.Tag):
32 if isinstance(commit, objects.Tag):
32 revision = commit.object[1]
33 revision = commit.object[1]
33 commit = self.repository._repo.get_object(commit.object[1])
34 commit = self.repository._repo.get_object(commit.object[1])
34 except KeyError:
35 except KeyError:
35 raise RepositoryError("Cannot get object with id %s" % revision)
36 raise RepositoryError("Cannot get object with id %s" % revision)
36 self.raw_id = revision
37 self.raw_id = revision
37 self.id = self.raw_id
38 self.id = self.raw_id
38 self.short_id = self.raw_id[:12]
39 self.short_id = self.raw_id[:12]
39 self._commit = commit
40 self._commit = commit
40
41
41 self._tree_id = commit.tree
42 self._tree_id = commit.tree
42 self._commiter_property = 'committer'
43 self._commiter_property = 'committer'
43 self._date_property = 'commit_time'
44 self._date_property = 'commit_time'
44 self._date_tz_property = 'commit_timezone'
45 self._date_tz_property = 'commit_timezone'
45 self.revision = repository.revisions.index(revision)
46 self.revision = repository.revisions.index(revision)
46
47
47 self.message = safe_unicode(commit.message)
48 self.message = safe_unicode(commit.message)
48 #self.branch = None
49 #self.branch = None
49 self.tags = []
50 self.tags = []
50 self.nodes = {}
51 self.nodes = {}
51 self._paths = {}
52 self._paths = {}
52
53
53 @LazyProperty
54 @LazyProperty
54 def author(self):
55 def author(self):
55 return safe_unicode(getattr(self._commit, self._commiter_property))
56 return safe_unicode(getattr(self._commit, self._commiter_property))
56
57
57 @LazyProperty
58 @LazyProperty
58 def date(self):
59 def date(self):
59 return date_fromtimestamp(getattr(self._commit, self._date_property),
60 return date_fromtimestamp(getattr(self._commit, self._date_property),
60 getattr(self._commit, self._date_tz_property))
61 getattr(self._commit, self._date_tz_property))
61
62
62 @LazyProperty
63 @LazyProperty
63 def _timestamp(self):
64 def _timestamp(self):
64 return getattr(self._commit, self._date_property)
65 return getattr(self._commit, self._date_property)
65
66
66 @LazyProperty
67 @LazyProperty
67 def status(self):
68 def status(self):
68 """
69 """
69 Returns modified, added, removed, deleted files for current changeset
70 Returns modified, added, removed, deleted files for current changeset
70 """
71 """
71 return self.changed, self.added, self.removed
72 return self.changed, self.added, self.removed
72
73
73 @LazyProperty
74 @LazyProperty
74 def branch(self):
75 def branch(self):
75
76
76 heads = self.repository._heads(reverse=False)
77 heads = self.repository._heads(reverse=False)
77
78
78 ref = heads.get(self.raw_id)
79 ref = heads.get(self.raw_id)
79 if ref:
80 if ref:
80 return safe_unicode(ref)
81 return safe_unicode(ref)
81
82
82 def _fix_path(self, path):
83 def _fix_path(self, path):
83 """
84 """
84 Paths are stored without trailing slash so we need to get rid off it if
85 Paths are stored without trailing slash so we need to get rid off it if
85 needed.
86 needed.
86 """
87 """
87 if path.endswith('/'):
88 if path.endswith('/'):
88 path = path.rstrip('/')
89 path = path.rstrip('/')
89 return path
90 return path
90
91
91 def _get_id_for_path(self, path):
92 def _get_id_for_path(self, path):
92
93
93 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
94 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
94 if not path in self._paths:
95 if not path in self._paths:
95 path = path.strip('/')
96 path = path.strip('/')
96 # set root tree
97 # set root tree
97 tree = self.repository._repo[self._tree_id]
98 tree = self.repository._repo[self._tree_id]
98 if path == '':
99 if path == '':
99 self._paths[''] = tree.id
100 self._paths[''] = tree.id
100 return tree.id
101 return tree.id
101 splitted = path.split('/')
102 splitted = path.split('/')
102 dirs, name = splitted[:-1], splitted[-1]
103 dirs, name = splitted[:-1], splitted[-1]
103 curdir = ''
104 curdir = ''
104
105
105 # initially extract things from root dir
106 # initially extract things from root dir
106 for item, stat, id in tree.iteritems():
107 for item, stat, id in tree.iteritems():
107 if curdir:
108 if curdir:
108 name = '/'.join((curdir, item))
109 name = '/'.join((curdir, item))
109 else:
110 else:
110 name = item
111 name = item
111 self._paths[name] = id
112 self._paths[name] = id
112 self._stat_modes[name] = stat
113 self._stat_modes[name] = stat
113
114
114 for dir in dirs:
115 for dir in dirs:
115 if curdir:
116 if curdir:
116 curdir = '/'.join((curdir, dir))
117 curdir = '/'.join((curdir, dir))
117 else:
118 else:
118 curdir = dir
119 curdir = dir
119 dir_id = None
120 dir_id = None
120 for item, stat, id in tree.iteritems():
121 for item, stat, id in tree.iteritems():
121 if dir == item:
122 if dir == item:
122 dir_id = id
123 dir_id = id
123 if dir_id:
124 if dir_id:
124 # Update tree
125 # Update tree
125 tree = self.repository._repo[dir_id]
126 tree = self.repository._repo[dir_id]
126 if not isinstance(tree, objects.Tree):
127 if not isinstance(tree, objects.Tree):
127 raise ChangesetError('%s is not a directory' % curdir)
128 raise ChangesetError('%s is not a directory' % curdir)
128 else:
129 else:
129 raise ChangesetError('%s have not been found' % curdir)
130 raise ChangesetError('%s have not been found' % curdir)
130
131
131 # cache all items from the given traversed tree
132 # cache all items from the given traversed tree
132 for item, stat, id in tree.iteritems():
133 for item, stat, id in tree.iteritems():
133 if curdir:
134 if curdir:
134 name = '/'.join((curdir, item))
135 name = '/'.join((curdir, item))
135 else:
136 else:
136 name = item
137 name = item
137 self._paths[name] = id
138 self._paths[name] = id
138 self._stat_modes[name] = stat
139 self._stat_modes[name] = stat
139 if not path in self._paths:
140 if not path in self._paths:
140 raise NodeDoesNotExistError("There is no file nor directory "
141 raise NodeDoesNotExistError("There is no file nor directory "
141 "at the given path %r at revision %r"
142 "at the given path %r at revision %r"
142 % (path, self.short_id))
143 % (path, self.short_id))
143 return self._paths[path]
144 return self._paths[path]
144
145
145 def _get_kind(self, path):
146 def _get_kind(self, path):
146 obj = self.repository._repo[self._get_id_for_path(path)]
147 obj = self.repository._repo[self._get_id_for_path(path)]
147 if isinstance(obj, objects.Blob):
148 if isinstance(obj, objects.Blob):
148 return NodeKind.FILE
149 return NodeKind.FILE
149 elif isinstance(obj, objects.Tree):
150 elif isinstance(obj, objects.Tree):
150 return NodeKind.DIR
151 return NodeKind.DIR
151
152
152 def _get_file_nodes(self):
153 def _get_file_nodes(self):
153 return chain(*(t[2] for t in self.walk()))
154 return chain(*(t[2] for t in self.walk()))
154
155
155 @LazyProperty
156 @LazyProperty
156 def parents(self):
157 def parents(self):
157 """
158 """
158 Returns list of parents changesets.
159 Returns list of parents changesets.
159 """
160 """
160 return [self.repository.get_changeset(parent)
161 return [self.repository.get_changeset(parent)
161 for parent in self._commit.parents]
162 for parent in self._commit.parents]
162
163
163 def next(self, branch=None):
164 def next(self, branch=None):
164
165
165 if branch and self.branch != branch:
166 if branch and self.branch != branch:
166 raise VCSError('Branch option used on changeset not belonging '
167 raise VCSError('Branch option used on changeset not belonging '
167 'to that branch')
168 'to that branch')
168
169
169 def _next(changeset, branch):
170 def _next(changeset, branch):
170 try:
171 try:
171 next_ = changeset.revision + 1
172 next_ = changeset.revision + 1
172 next_rev = changeset.repository.revisions[next_]
173 next_rev = changeset.repository.revisions[next_]
173 except IndexError:
174 except IndexError:
174 raise ChangesetDoesNotExistError
175 raise ChangesetDoesNotExistError
175 cs = changeset.repository.get_changeset(next_rev)
176 cs = changeset.repository.get_changeset(next_rev)
176
177
177 if branch and branch != cs.branch:
178 if branch and branch != cs.branch:
178 return _next(cs, branch)
179 return _next(cs, branch)
179
180
180 return cs
181 return cs
181
182
182 return _next(self, branch)
183 return _next(self, branch)
183
184
184 def prev(self, branch=None):
185 def prev(self, branch=None):
185 if branch and self.branch != branch:
186 if branch and self.branch != branch:
186 raise VCSError('Branch option used on changeset not belonging '
187 raise VCSError('Branch option used on changeset not belonging '
187 'to that branch')
188 'to that branch')
188
189
189 def _prev(changeset, branch):
190 def _prev(changeset, branch):
190 try:
191 try:
191 prev_ = changeset.revision - 1
192 prev_ = changeset.revision - 1
192 if prev_ < 0:
193 if prev_ < 0:
193 raise IndexError
194 raise IndexError
194 prev_rev = changeset.repository.revisions[prev_]
195 prev_rev = changeset.repository.revisions[prev_]
195 except IndexError:
196 except IndexError:
196 raise ChangesetDoesNotExistError
197 raise ChangesetDoesNotExistError
197
198
198 cs = changeset.repository.get_changeset(prev_rev)
199 cs = changeset.repository.get_changeset(prev_rev)
199
200
200 if branch and branch != cs.branch:
201 if branch and branch != cs.branch:
201 return _prev(cs, branch)
202 return _prev(cs, branch)
202
203
203 return cs
204 return cs
204
205
205 return _prev(self, branch)
206 return _prev(self, branch)
206
207
207 def diff(self, ignore_whitespace=True, context=3):
208 def diff(self, ignore_whitespace=True, context=3):
208 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
209 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
209 rev2 = self
210 rev2 = self
210 return ''.join(self.repository.get_diff(rev1, rev2,
211 return ''.join(self.repository.get_diff(rev1, rev2,
211 ignore_whitespace=ignore_whitespace,
212 ignore_whitespace=ignore_whitespace,
212 context=context))
213 context=context))
213
214
214 def get_file_mode(self, path):
215 def get_file_mode(self, path):
215 """
216 """
216 Returns stat mode of the file at the given ``path``.
217 Returns stat mode of the file at the given ``path``.
217 """
218 """
218 # ensure path is traversed
219 # ensure path is traversed
219 self._get_id_for_path(path)
220 self._get_id_for_path(path)
220 return self._stat_modes[path]
221 return self._stat_modes[path]
221
222
222 def get_file_content(self, path):
223 def get_file_content(self, path):
223 """
224 """
224 Returns content of the file at given ``path``.
225 Returns content of the file at given ``path``.
225 """
226 """
226 id = self._get_id_for_path(path)
227 id = self._get_id_for_path(path)
227 blob = self.repository._repo[id]
228 blob = self.repository._repo[id]
228 return blob.as_pretty_string()
229 return blob.as_pretty_string()
229
230
230 def get_file_size(self, path):
231 def get_file_size(self, path):
231 """
232 """
232 Returns size of the file at given ``path``.
233 Returns size of the file at given ``path``.
233 """
234 """
234 id = self._get_id_for_path(path)
235 id = self._get_id_for_path(path)
235 blob = self.repository._repo[id]
236 blob = self.repository._repo[id]
236 return blob.raw_length()
237 return blob.raw_length()
237
238
238 def get_file_changeset(self, path):
239 def get_file_changeset(self, path):
239 """
240 """
240 Returns last commit of the file at the given ``path``.
241 Returns last commit of the file at the given ``path``.
241 """
242 """
242 node = self.get_node(path)
243 node = self.get_node(path)
243 return node.history[0]
244 return node.history[0]
244
245
245 def get_file_history(self, path):
246 def get_file_history(self, path):
246 """
247 """
247 Returns history of file as reversed list of ``Changeset`` objects for
248 Returns history of file as reversed list of ``Changeset`` objects for
248 which file at given ``path`` has been modified.
249 which file at given ``path`` has been modified.
249
250
250 TODO: This function now uses os underlying 'git' and 'grep' commands
251 TODO: This function now uses os underlying 'git' and 'grep' commands
251 which is generally not good. Should be replaced with algorithm
252 which is generally not good. Should be replaced with algorithm
252 iterating commits.
253 iterating commits.
253 """
254 """
254 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
255 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
255 self.id, path
256 self.id, path
256 )
257 )
257 so, se = self.repository.run_git_command(cmd)
258 so, se = self.repository.run_git_command(cmd)
258 ids = re.findall(r'[0-9a-fA-F]{40}', so)
259 ids = re.findall(r'[0-9a-fA-F]{40}', so)
259 return [self.repository.get_changeset(id) for id in ids]
260 return [self.repository.get_changeset(id) for id in ids]
260
261
261 def get_file_annotate(self, path):
262 def get_file_annotate(self, path):
262 """
263 """
263 Returns a list of three element tuples with lineno,changeset and line
264 Returns a list of three element tuples with lineno,changeset and line
264
265
265 TODO: This function now uses os underlying 'git' command which is
266 TODO: This function now uses os underlying 'git' command which is
266 generally not good. Should be replaced with algorithm iterating
267 generally not good. Should be replaced with algorithm iterating
267 commits.
268 commits.
268 """
269 """
269 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
270 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
270 # -l ==> outputs long shas (and we need all 40 characters)
271 # -l ==> outputs long shas (and we need all 40 characters)
271 # --root ==> doesn't put '^' character for bounderies
272 # --root ==> doesn't put '^' character for bounderies
272 # -r sha ==> blames for the given revision
273 # -r sha ==> blames for the given revision
273 so, se = self.repository.run_git_command(cmd)
274 so, se = self.repository.run_git_command(cmd)
274
275
275 annotate = []
276 annotate = []
276 for i, blame_line in enumerate(so.split('\n')[:-1]):
277 for i, blame_line in enumerate(so.split('\n')[:-1]):
277 ln_no = i + 1
278 ln_no = i + 1
278 id, line = re.split(r' ', blame_line, 1)
279 id, line = re.split(r' ', blame_line, 1)
279 annotate.append((ln_no, self.repository.get_changeset(id), line))
280 annotate.append((ln_no, self.repository.get_changeset(id), line))
280 return annotate
281 return annotate
281
282
282 def fill_archive(self, stream=None, kind='tgz', prefix=None,
283 def fill_archive(self, stream=None, kind='tgz', prefix=None,
283 subrepos=False):
284 subrepos=False):
284 """
285 """
285 Fills up given stream.
286 Fills up given stream.
286
287
287 :param stream: file like object.
288 :param stream: file like object.
288 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
289 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
289 Default: ``tgz``.
290 Default: ``tgz``.
290 :param prefix: name of root directory in archive.
291 :param prefix: name of root directory in archive.
291 Default is repository name and changeset's raw_id joined with dash
292 Default is repository name and changeset's raw_id joined with dash
292 (``repo-tip.<KIND>``).
293 (``repo-tip.<KIND>``).
293 :param subrepos: include subrepos in this archive.
294 :param subrepos: include subrepos in this archive.
294
295
295 :raise ImproperArchiveTypeError: If given kind is wrong.
296 :raise ImproperArchiveTypeError: If given kind is wrong.
296 :raise VcsError: If given stream is None
297 :raise VcsError: If given stream is None
297
298
298 """
299 """
299 allowed_kinds = settings.ARCHIVE_SPECS.keys()
300 allowed_kinds = settings.ARCHIVE_SPECS.keys()
300 if kind not in allowed_kinds:
301 if kind not in allowed_kinds:
301 raise ImproperArchiveTypeError('Archive kind not supported use one'
302 raise ImproperArchiveTypeError('Archive kind not supported use one'
302 'of %s', allowed_kinds)
303 'of %s', allowed_kinds)
303
304
304 if prefix is None:
305 if prefix is None:
305 prefix = '%s-%s' % (self.repository.name, self.short_id)
306 prefix = '%s-%s' % (self.repository.name, self.short_id)
306 elif prefix.startswith('/'):
307 elif prefix.startswith('/'):
307 raise VCSError("Prefix cannot start with leading slash")
308 raise VCSError("Prefix cannot start with leading slash")
308 elif prefix.strip() == '':
309 elif prefix.strip() == '':
309 raise VCSError("Prefix cannot be empty")
310 raise VCSError("Prefix cannot be empty")
310
311
311 if kind == 'zip':
312 if kind == 'zip':
312 frmt = 'zip'
313 frmt = 'zip'
313 else:
314 else:
314 frmt = 'tar'
315 frmt = 'tar'
315 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
316 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
316 self.raw_id)
317 self.raw_id)
317 if kind == 'tgz':
318 if kind == 'tgz':
318 cmd += ' | gzip -9'
319 cmd += ' | gzip -9'
319 elif kind == 'tbz2':
320 elif kind == 'tbz2':
320 cmd += ' | bzip2 -9'
321 cmd += ' | bzip2 -9'
321
322
322 if stream is None:
323 if stream is None:
323 raise VCSError('You need to pass in a valid stream for filling'
324 raise VCSError('You need to pass in a valid stream for filling'
324 ' with archival data')
325 ' with archival data')
325 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
326 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
326 cwd=self.repository.path)
327 cwd=self.repository.path)
327
328
328 buffer_size = 1024 * 8
329 buffer_size = 1024 * 8
329 chunk = popen.stdout.read(buffer_size)
330 chunk = popen.stdout.read(buffer_size)
330 while chunk:
331 while chunk:
331 stream.write(chunk)
332 stream.write(chunk)
332 chunk = popen.stdout.read(buffer_size)
333 chunk = popen.stdout.read(buffer_size)
333 # Make sure all descriptors would be read
334 # Make sure all descriptors would be read
334 popen.communicate()
335 popen.communicate()
335
336
336 def get_nodes(self, path):
337 def get_nodes(self, path):
337 if self._get_kind(path) != NodeKind.DIR:
338 if self._get_kind(path) != NodeKind.DIR:
338 raise ChangesetError("Directory does not exist for revision %r at "
339 raise ChangesetError("Directory does not exist for revision %r at "
339 " %r" % (self.revision, path))
340 " %r" % (self.revision, path))
340 path = self._fix_path(path)
341 path = self._fix_path(path)
341 id = self._get_id_for_path(path)
342 id = self._get_id_for_path(path)
342 tree = self.repository._repo[id]
343 tree = self.repository._repo[id]
343 dirnodes = []
344 dirnodes = []
344 filenodes = []
345 filenodes = []
345 als = self.repository.alias
346 als = self.repository.alias
346 for name, stat, id in tree.iteritems():
347 for name, stat, id in tree.iteritems():
347 if objects.S_ISGITLINK(stat):
348 if objects.S_ISGITLINK(stat):
348 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
349 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
349 alias=als))
350 alias=als))
350 continue
351 continue
351
352
352 obj = self.repository._repo.get_object(id)
353 obj = self.repository._repo.get_object(id)
353 if path != '':
354 if path != '':
354 obj_path = '/'.join((path, name))
355 obj_path = '/'.join((path, name))
355 else:
356 else:
356 obj_path = name
357 obj_path = name
357 if obj_path not in self._stat_modes:
358 if obj_path not in self._stat_modes:
358 self._stat_modes[obj_path] = stat
359 self._stat_modes[obj_path] = stat
359 if isinstance(obj, objects.Tree):
360 if isinstance(obj, objects.Tree):
360 dirnodes.append(DirNode(obj_path, changeset=self))
361 dirnodes.append(DirNode(obj_path, changeset=self))
361 elif isinstance(obj, objects.Blob):
362 elif isinstance(obj, objects.Blob):
362 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
363 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
363 else:
364 else:
364 raise ChangesetError("Requested object should be Tree "
365 raise ChangesetError("Requested object should be Tree "
365 "or Blob, is %r" % type(obj))
366 "or Blob, is %r" % type(obj))
366 nodes = dirnodes + filenodes
367 nodes = dirnodes + filenodes
367 for node in nodes:
368 for node in nodes:
368 if not node.path in self.nodes:
369 if not node.path in self.nodes:
369 self.nodes[node.path] = node
370 self.nodes[node.path] = node
370 nodes.sort()
371 nodes.sort()
371 return nodes
372 return nodes
372
373
373 def get_node(self, path):
374 def get_node(self, path):
374 if isinstance(path, unicode):
375 if isinstance(path, unicode):
375 path = path.encode('utf-8')
376 path = path.encode('utf-8')
376 path = self._fix_path(path)
377 path = self._fix_path(path)
377 if not path in self.nodes:
378 if not path in self.nodes:
378 try:
379 try:
379 id_ = self._get_id_for_path(path)
380 id_ = self._get_id_for_path(path)
380 except ChangesetError:
381 except ChangesetError:
381 raise NodeDoesNotExistError("Cannot find one of parents' "
382 raise NodeDoesNotExistError("Cannot find one of parents' "
382 "directories for a given path: %s" % path)
383 "directories for a given path: %s" % path)
383
384
384 _GL = lambda m: m and objects.S_ISGITLINK(m)
385 _GL = lambda m: m and objects.S_ISGITLINK(m)
385 if _GL(self._stat_modes.get(path)):
386 if _GL(self._stat_modes.get(path)):
386 node = SubModuleNode(path, url=None, changeset=id_,
387 node = SubModuleNode(path, url=None, changeset=id_,
387 alias=self.repository.alias)
388 alias=self.repository.alias)
388 else:
389 else:
389 obj = self.repository._repo.get_object(id_)
390 obj = self.repository._repo.get_object(id_)
390
391
391 if isinstance(obj, objects.Tree):
392 if isinstance(obj, objects.Tree):
392 if path == '':
393 if path == '':
393 node = RootNode(changeset=self)
394 node = RootNode(changeset=self)
394 else:
395 else:
395 node = DirNode(path, changeset=self)
396 node = DirNode(path, changeset=self)
396 node._tree = obj
397 node._tree = obj
397 elif isinstance(obj, objects.Blob):
398 elif isinstance(obj, objects.Blob):
398 node = FileNode(path, changeset=self)
399 node = FileNode(path, changeset=self)
399 node._blob = obj
400 node._blob = obj
400 else:
401 else:
401 raise NodeDoesNotExistError("There is no file nor directory "
402 raise NodeDoesNotExistError("There is no file nor directory "
402 "at the given path %r at revision %r"
403 "at the given path %r at revision %r"
403 % (path, self.short_id))
404 % (path, self.short_id))
404 # cache node
405 # cache node
405 self.nodes[path] = node
406 self.nodes[path] = node
406 return self.nodes[path]
407 return self.nodes[path]
407
408
408 @LazyProperty
409 @LazyProperty
409 def affected_files(self):
410 def affected_files(self):
410 """
411 """
411 Get's a fast accessible file changes for given changeset
412 Get's a fast accessible file changes for given changeset
412 """
413 """
413 a, m, d = self._changes_cache
414 a, m, d = self._changes_cache
414 return list(a.union(m).union(d))
415 return list(a.union(m).union(d))
415
416
416 @LazyProperty
417 @LazyProperty
417 def _diff_name_status(self):
418 def _diff_name_status(self):
418 output = []
419 output = []
419 for parent in self.parents:
420 for parent in self.parents:
420 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id,
421 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id,
421 self.raw_id)
422 self.raw_id)
422 so, se = self.repository.run_git_command(cmd)
423 so, se = self.repository.run_git_command(cmd)
423 output.append(so.strip())
424 output.append(so.strip())
424 return '\n'.join(output)
425 return '\n'.join(output)
425
426
426 @LazyProperty
427 @LazyProperty
427 def _changes_cache(self):
428 def _changes_cache(self):
428 added = set()
429 added = set()
429 modified = set()
430 modified = set()
430 deleted = set()
431 deleted = set()
431 _r = self.repository._repo
432 _r = self.repository._repo
432
433
433 parents = self.parents
434 parents = self.parents
434 if not self.parents:
435 if not self.parents:
435 parents = [EmptyChangeset()]
436 parents = [EmptyChangeset()]
436 for parent in parents:
437 for parent in parents:
437 if isinstance(parent, EmptyChangeset):
438 if isinstance(parent, EmptyChangeset):
438 oid = None
439 oid = None
439 else:
440 else:
440 oid = _r[parent.raw_id].tree
441 oid = _r[parent.raw_id].tree
441 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
442 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
442 for (oldpath, newpath), (_, _), (_, _) in changes:
443 for (oldpath, newpath), (_, _), (_, _) in changes:
443 if newpath and oldpath:
444 if newpath and oldpath:
444 modified.add(newpath)
445 modified.add(newpath)
445 elif newpath and not oldpath:
446 elif newpath and not oldpath:
446 added.add(newpath)
447 added.add(newpath)
447 elif not newpath and oldpath:
448 elif not newpath and oldpath:
448 deleted.add(oldpath)
449 deleted.add(oldpath)
449 return added, modified, deleted
450 return added, modified, deleted
450
451
451 def _get_paths_for_status(self, status):
452 def _get_paths_for_status(self, status):
452 """
453 """
453 Returns sorted list of paths for given ``status``.
454 Returns sorted list of paths for given ``status``.
454
455
455 :param status: one of: *added*, *modified* or *deleted*
456 :param status: one of: *added*, *modified* or *deleted*
456 """
457 """
457 a, m, d = self._changes_cache
458 a, m, d = self._changes_cache
458 return sorted({
459 return sorted({
459 'added': list(a),
460 'added': list(a),
460 'modified': list(m),
461 'modified': list(m),
461 'deleted': list(d)}[status]
462 'deleted': list(d)}[status]
462 )
463 )
463
464
464 @LazyProperty
465 @LazyProperty
465 def added(self):
466 def added(self):
466 """
467 """
467 Returns list of added ``FileNode`` objects.
468 Returns list of added ``FileNode`` objects.
468 """
469 """
469 if not self.parents:
470 if not self.parents:
470 return list(self._get_file_nodes())
471 return list(self._get_file_nodes())
471 return [self.get_node(path) for path in self._get_paths_for_status('added')]
472 return AddedFileNodesGenerator([n for n in
473 self._get_paths_for_status('added')], self)
472
474
473 @LazyProperty
475 @LazyProperty
474 def changed(self):
476 def changed(self):
475 """
477 """
476 Returns list of modified ``FileNode`` objects.
478 Returns list of modified ``FileNode`` objects.
477 """
479 """
478 if not self.parents:
480 if not self.parents:
479 return []
481 return []
480 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
482 return ChangedFileNodesGenerator([n for n in
483 self._get_paths_for_status('modified')], self)
481
484
482 @LazyProperty
485 @LazyProperty
483 def removed(self):
486 def removed(self):
484 """
487 """
485 Returns list of removed ``FileNode`` objects.
488 Returns list of removed ``FileNode`` objects.
486 """
489 """
487 if not self.parents:
490 if not self.parents:
488 return []
491 return []
489 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
492 return RemovedFileNodesGenerator([n for n in
493 self._get_paths_for_status('deleted')], self)
@@ -1,344 +1,348 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, AddedFileNodesGenerator,\
9 ChangedFileNodesGenerator, RemovedFileNodesGenerator
9 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
10 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 from rhodecode.lib.vcs.exceptions import RepositoryError
12 from rhodecode.lib.vcs.exceptions import RepositoryError
12 from rhodecode.lib.vcs.utils.compat import unittest
13 from rhodecode.lib.vcs.utils.compat import unittest
13
14
14
15
15 class TestBaseChangeset(unittest.TestCase):
16 class TestBaseChangeset(unittest.TestCase):
16
17
17 def test_as_dict(self):
18 def test_as_dict(self):
18 changeset = BaseChangeset()
19 changeset = BaseChangeset()
19 changeset.id = 'ID'
20 changeset.id = 'ID'
20 changeset.raw_id = 'RAW_ID'
21 changeset.raw_id = 'RAW_ID'
21 changeset.short_id = 'SHORT_ID'
22 changeset.short_id = 'SHORT_ID'
22 changeset.revision = 1009
23 changeset.revision = 1009
23 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
24 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
24 changeset.message = 'Message of a commit'
25 changeset.message = 'Message of a commit'
25 changeset.author = 'Joe Doe <joe.doe@example.com>'
26 changeset.author = 'Joe Doe <joe.doe@example.com>'
26 changeset.added = [FileNode('foo/bar/baz'), FileNode('foobar')]
27 changeset.added = [FileNode('foo/bar/baz'), FileNode('foobar')]
27 changeset.changed = []
28 changeset.changed = []
28 changeset.removed = []
29 changeset.removed = []
29 self.assertEqual(changeset.as_dict(), {
30 self.assertEqual(changeset.as_dict(), {
30 'id': 'ID',
31 'id': 'ID',
31 'raw_id': 'RAW_ID',
32 'raw_id': 'RAW_ID',
32 'short_id': 'SHORT_ID',
33 'short_id': 'SHORT_ID',
33 'revision': 1009,
34 'revision': 1009,
34 'date': datetime.datetime(2011, 1, 30, 1, 45),
35 'date': datetime.datetime(2011, 1, 30, 1, 45),
35 'message': 'Message of a commit',
36 'message': 'Message of a commit',
36 'author': {
37 'author': {
37 'name': 'Joe Doe',
38 'name': 'Joe Doe',
38 'email': 'joe.doe@example.com',
39 'email': 'joe.doe@example.com',
39 },
40 },
40 'added': ['foo/bar/baz', 'foobar'],
41 'added': ['foo/bar/baz', 'foobar'],
41 'changed': [],
42 'changed': [],
42 'removed': [],
43 'removed': [],
43 })
44 })
44
45
45 class ChangesetsWithCommitsTestCaseixin(BackendTestMixin):
46 class ChangesetsWithCommitsTestCaseixin(BackendTestMixin):
46 recreate_repo_per_test = True
47 recreate_repo_per_test = True
47
48
48 @classmethod
49 @classmethod
49 def _get_commits(cls):
50 def _get_commits(cls):
50 start_date = datetime.datetime(2010, 1, 1, 20)
51 start_date = datetime.datetime(2010, 1, 1, 20)
51 for x in xrange(5):
52 for x in xrange(5):
52 yield {
53 yield {
53 'message': 'Commit %d' % x,
54 'message': 'Commit %d' % x,
54 'author': 'Joe Doe <joe.doe@example.com>',
55 'author': 'Joe Doe <joe.doe@example.com>',
55 'date': start_date + datetime.timedelta(hours=12 * x),
56 'date': start_date + datetime.timedelta(hours=12 * x),
56 'added': [
57 'added': [
57 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
58 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
58 ],
59 ],
59 }
60 }
60
61
61 def test_new_branch(self):
62 def test_new_branch(self):
62 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
63 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
63 content='Documentation\n'))
64 content='Documentation\n'))
64 foobar_tip = self.imc.commit(
65 foobar_tip = self.imc.commit(
65 message=u'New branch: foobar',
66 message=u'New branch: foobar',
66 author=u'joe',
67 author=u'joe',
67 branch='foobar',
68 branch='foobar',
68 )
69 )
69 self.assertTrue('foobar' in self.repo.branches)
70 self.assertTrue('foobar' in self.repo.branches)
70 self.assertEqual(foobar_tip.branch, 'foobar')
71 self.assertEqual(foobar_tip.branch, 'foobar')
71 # 'foobar' should be the only branch that contains the new commit
72 # 'foobar' should be the only branch that contains the new commit
72 self.assertNotEqual(*self.repo.branches.values())
73 self.assertNotEqual(*self.repo.branches.values())
73
74
74 def test_new_head_in_default_branch(self):
75 def test_new_head_in_default_branch(self):
75 tip = self.repo.get_changeset()
76 tip = self.repo.get_changeset()
76 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
77 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
77 content='Documentation\n'))
78 content='Documentation\n'))
78 foobar_tip = self.imc.commit(
79 foobar_tip = self.imc.commit(
79 message=u'New branch: foobar',
80 message=u'New branch: foobar',
80 author=u'joe',
81 author=u'joe',
81 branch='foobar',
82 branch='foobar',
82 parents=[tip],
83 parents=[tip],
83 )
84 )
84 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
85 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
85 content='Documentation\nand more...\n'))
86 content='Documentation\nand more...\n'))
86 newtip = self.imc.commit(
87 newtip = self.imc.commit(
87 message=u'At default branch',
88 message=u'At default branch',
88 author=u'joe',
89 author=u'joe',
89 branch=foobar_tip.branch,
90 branch=foobar_tip.branch,
90 parents=[foobar_tip],
91 parents=[foobar_tip],
91 )
92 )
92
93
93 newest_tip = self.imc.commit(
94 newest_tip = self.imc.commit(
94 message=u'Merged with %s' % foobar_tip.raw_id,
95 message=u'Merged with %s' % foobar_tip.raw_id,
95 author=u'joe',
96 author=u'joe',
96 branch=self.backend_class.DEFAULT_BRANCH_NAME,
97 branch=self.backend_class.DEFAULT_BRANCH_NAME,
97 parents=[newtip, foobar_tip],
98 parents=[newtip, foobar_tip],
98 )
99 )
99
100
100 self.assertEqual(newest_tip.branch,
101 self.assertEqual(newest_tip.branch,
101 self.backend_class.DEFAULT_BRANCH_NAME)
102 self.backend_class.DEFAULT_BRANCH_NAME)
102
103
103 def test_get_changesets_respects_branch_name(self):
104 def test_get_changesets_respects_branch_name(self):
104 tip = self.repo.get_changeset()
105 tip = self.repo.get_changeset()
105 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
106 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
106 content='Documentation\n'))
107 content='Documentation\n'))
107 doc_changeset = self.imc.commit(
108 doc_changeset = self.imc.commit(
108 message=u'New branch: docs',
109 message=u'New branch: docs',
109 author=u'joe',
110 author=u'joe',
110 branch='docs',
111 branch='docs',
111 )
112 )
112 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
113 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
113 self.imc.commit(
114 self.imc.commit(
114 message=u'Back in default branch',
115 message=u'Back in default branch',
115 author=u'joe',
116 author=u'joe',
116 parents=[tip],
117 parents=[tip],
117 )
118 )
118 default_branch_changesets = self.repo.get_changesets(
119 default_branch_changesets = self.repo.get_changesets(
119 branch_name=self.repo.DEFAULT_BRANCH_NAME)
120 branch_name=self.repo.DEFAULT_BRANCH_NAME)
120 self.assertNotIn(doc_changeset, default_branch_changesets)
121 self.assertNotIn(doc_changeset, default_branch_changesets)
121
122
122 def test_get_changeset_by_branch(self):
123 def test_get_changeset_by_branch(self):
123 for branch, sha in self.repo.branches.iteritems():
124 for branch, sha in self.repo.branches.iteritems():
124 self.assertEqual(sha, self.repo.get_changeset(branch).raw_id)
125 self.assertEqual(sha, self.repo.get_changeset(branch).raw_id)
125
126
126 def test_get_changeset_by_tag(self):
127 def test_get_changeset_by_tag(self):
127 for tag, sha in self.repo.tags.iteritems():
128 for tag, sha in self.repo.tags.iteritems():
128 self.assertEqual(sha, self.repo.get_changeset(tag).raw_id)
129 self.assertEqual(sha, self.repo.get_changeset(tag).raw_id)
129
130
130
131
131 class ChangesetsTestCaseMixin(BackendTestMixin):
132 class ChangesetsTestCaseMixin(BackendTestMixin):
132 recreate_repo_per_test = False
133 recreate_repo_per_test = False
133
134
134 @classmethod
135 @classmethod
135 def _get_commits(cls):
136 def _get_commits(cls):
136 start_date = datetime.datetime(2010, 1, 1, 20)
137 start_date = datetime.datetime(2010, 1, 1, 20)
137 for x in xrange(5):
138 for x in xrange(5):
138 yield {
139 yield {
139 'message': u'Commit %d' % x,
140 'message': u'Commit %d' % x,
140 'author': u'Joe Doe <joe.doe@example.com>',
141 'author': u'Joe Doe <joe.doe@example.com>',
141 'date': start_date + datetime.timedelta(hours=12 * x),
142 'date': start_date + datetime.timedelta(hours=12 * x),
142 'added': [
143 'added': [
143 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
144 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
144 ],
145 ],
145 }
146 }
146
147
147 def test_simple(self):
148 def test_simple(self):
148 tip = self.repo.get_changeset()
149 tip = self.repo.get_changeset()
149 self.assertEqual(tip.date, datetime.datetime(2010, 1, 3, 20))
150 self.assertEqual(tip.date, datetime.datetime(2010, 1, 3, 20))
150
151
151 def test_get_changesets_is_ordered_by_date(self):
152 def test_get_changesets_is_ordered_by_date(self):
152 changesets = list(self.repo.get_changesets())
153 changesets = list(self.repo.get_changesets())
153 ordered_by_date = sorted(changesets,
154 ordered_by_date = sorted(changesets,
154 key=lambda cs: cs.date)
155 key=lambda cs: cs.date)
155 self.assertItemsEqual(changesets, ordered_by_date)
156 self.assertItemsEqual(changesets, ordered_by_date)
156
157
157 def test_get_changesets_respects_start(self):
158 def test_get_changesets_respects_start(self):
158 second_id = self.repo.revisions[1]
159 second_id = self.repo.revisions[1]
159 changesets = list(self.repo.get_changesets(start=second_id))
160 changesets = list(self.repo.get_changesets(start=second_id))
160 self.assertEqual(len(changesets), 4)
161 self.assertEqual(len(changesets), 4)
161
162
162 def test_get_changesets_numerical_id_respects_start(self):
163 def test_get_changesets_numerical_id_respects_start(self):
163 second_id = 1
164 second_id = 1
164 changesets = list(self.repo.get_changesets(start=second_id))
165 changesets = list(self.repo.get_changesets(start=second_id))
165 self.assertEqual(len(changesets), 4)
166 self.assertEqual(len(changesets), 4)
166
167
167 def test_get_changesets_includes_start_changeset(self):
168 def test_get_changesets_includes_start_changeset(self):
168 second_id = self.repo.revisions[1]
169 second_id = self.repo.revisions[1]
169 changesets = list(self.repo.get_changesets(start=second_id))
170 changesets = list(self.repo.get_changesets(start=second_id))
170 self.assertEqual(changesets[0].raw_id, second_id)
171 self.assertEqual(changesets[0].raw_id, second_id)
171
172
172 def test_get_changesets_respects_end(self):
173 def test_get_changesets_respects_end(self):
173 second_id = self.repo.revisions[1]
174 second_id = self.repo.revisions[1]
174 changesets = list(self.repo.get_changesets(end=second_id))
175 changesets = list(self.repo.get_changesets(end=second_id))
175 self.assertEqual(changesets[-1].raw_id, second_id)
176 self.assertEqual(changesets[-1].raw_id, second_id)
176 self.assertEqual(len(changesets), 2)
177 self.assertEqual(len(changesets), 2)
177
178
178 def test_get_changesets_numerical_id_respects_end(self):
179 def test_get_changesets_numerical_id_respects_end(self):
179 second_id = 1
180 second_id = 1
180 changesets = list(self.repo.get_changesets(end=second_id))
181 changesets = list(self.repo.get_changesets(end=second_id))
181 self.assertEqual(changesets.index(changesets[-1]), second_id)
182 self.assertEqual(changesets.index(changesets[-1]), second_id)
182 self.assertEqual(len(changesets), 2)
183 self.assertEqual(len(changesets), 2)
183
184
184 def test_get_changesets_respects_both_start_and_end(self):
185 def test_get_changesets_respects_both_start_and_end(self):
185 second_id = self.repo.revisions[1]
186 second_id = self.repo.revisions[1]
186 third_id = self.repo.revisions[2]
187 third_id = self.repo.revisions[2]
187 changesets = list(self.repo.get_changesets(start=second_id,
188 changesets = list(self.repo.get_changesets(start=second_id,
188 end=third_id))
189 end=third_id))
189 self.assertEqual(len(changesets), 2)
190 self.assertEqual(len(changesets), 2)
190
191
191 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
192 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
192 changesets = list(self.repo.get_changesets(start=2, end=3))
193 changesets = list(self.repo.get_changesets(start=2, end=3))
193 self.assertEqual(len(changesets), 2)
194 self.assertEqual(len(changesets), 2)
194
195
195 def test_get_changesets_includes_end_changeset(self):
196 def test_get_changesets_includes_end_changeset(self):
196 second_id = self.repo.revisions[1]
197 second_id = self.repo.revisions[1]
197 changesets = list(self.repo.get_changesets(end=second_id))
198 changesets = list(self.repo.get_changesets(end=second_id))
198 self.assertEqual(changesets[-1].raw_id, second_id)
199 self.assertEqual(changesets[-1].raw_id, second_id)
199
200
200 def test_get_changesets_respects_start_date(self):
201 def test_get_changesets_respects_start_date(self):
201 start_date = datetime.datetime(2010, 2, 1)
202 start_date = datetime.datetime(2010, 2, 1)
202 for cs in self.repo.get_changesets(start_date=start_date):
203 for cs in self.repo.get_changesets(start_date=start_date):
203 self.assertGreaterEqual(cs.date, start_date)
204 self.assertGreaterEqual(cs.date, start_date)
204
205
205 def test_get_changesets_respects_end_date(self):
206 def test_get_changesets_respects_end_date(self):
206 end_date = datetime.datetime(2010, 2, 1)
207 end_date = datetime.datetime(2010, 2, 1)
207 for cs in self.repo.get_changesets(end_date=end_date):
208 for cs in self.repo.get_changesets(end_date=end_date):
208 self.assertLessEqual(cs.date, end_date)
209 self.assertLessEqual(cs.date, end_date)
209
210
210 def test_get_changesets_respects_reverse(self):
211 def test_get_changesets_respects_reverse(self):
211 changesets_id_list = [cs.raw_id for cs in
212 changesets_id_list = [cs.raw_id for cs in
212 self.repo.get_changesets(reverse=True)]
213 self.repo.get_changesets(reverse=True)]
213 self.assertItemsEqual(changesets_id_list, reversed(self.repo.revisions))
214 self.assertItemsEqual(changesets_id_list, reversed(self.repo.revisions))
214
215
215 def test_get_filenodes_generator(self):
216 def test_get_filenodes_generator(self):
216 tip = self.repo.get_changeset()
217 tip = self.repo.get_changeset()
217 filepaths = [node.path for node in tip.get_filenodes_generator()]
218 filepaths = [node.path for node in tip.get_filenodes_generator()]
218 self.assertItemsEqual(filepaths, ['file_%d.txt' % x for x in xrange(5)])
219 self.assertItemsEqual(filepaths, ['file_%d.txt' % x for x in xrange(5)])
219
220
220 def test_size(self):
221 def test_size(self):
221 tip = self.repo.get_changeset()
222 tip = self.repo.get_changeset()
222 size = 5 * len('Foobar N') # Size of 5 files
223 size = 5 * len('Foobar N') # Size of 5 files
223 self.assertEqual(tip.size, size)
224 self.assertEqual(tip.size, size)
224
225
225 def test_author(self):
226 def test_author(self):
226 tip = self.repo.get_changeset()
227 tip = self.repo.get_changeset()
227 self.assertEqual(tip.author, u'Joe Doe <joe.doe@example.com>')
228 self.assertEqual(tip.author, u'Joe Doe <joe.doe@example.com>')
228
229
229 def test_author_name(self):
230 def test_author_name(self):
230 tip = self.repo.get_changeset()
231 tip = self.repo.get_changeset()
231 self.assertEqual(tip.author_name, u'Joe Doe')
232 self.assertEqual(tip.author_name, u'Joe Doe')
232
233
233 def test_author_email(self):
234 def test_author_email(self):
234 tip = self.repo.get_changeset()
235 tip = self.repo.get_changeset()
235 self.assertEqual(tip.author_email, u'joe.doe@example.com')
236 self.assertEqual(tip.author_email, u'joe.doe@example.com')
236
237
237 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
238 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
238 with self.assertRaises(ChangesetDoesNotExistError):
239 with self.assertRaises(ChangesetDoesNotExistError):
239 list(self.repo.get_changesets(start='foobar'))
240 list(self.repo.get_changesets(start='foobar'))
240
241
241 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
242 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
242 with self.assertRaises(ChangesetDoesNotExistError):
243 with self.assertRaises(ChangesetDoesNotExistError):
243 list(self.repo.get_changesets(end='foobar'))
244 list(self.repo.get_changesets(end='foobar'))
244
245
245 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
246 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
246 with self.assertRaises(BranchDoesNotExistError):
247 with self.assertRaises(BranchDoesNotExistError):
247 list(self.repo.get_changesets(branch_name='foobar'))
248 list(self.repo.get_changesets(branch_name='foobar'))
248
249
249 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
250 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
250 start = self.repo.revisions[-1]
251 start = self.repo.revisions[-1]
251 end = self.repo.revisions[0]
252 end = self.repo.revisions[0]
252 with self.assertRaises(RepositoryError):
253 with self.assertRaises(RepositoryError):
253 list(self.repo.get_changesets(start=start, end=end))
254 list(self.repo.get_changesets(start=start, end=end))
254
255
255 def test_get_changesets_numerical_id_reversed(self):
256 def test_get_changesets_numerical_id_reversed(self):
256 with self.assertRaises(RepositoryError):
257 with self.assertRaises(RepositoryError):
257 [x for x in self.repo.get_changesets(start=3, end=2)]
258 [x for x in self.repo.get_changesets(start=3, end=2)]
258
259
259 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
260 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
260 with self.assertRaises(RepositoryError):
261 with self.assertRaises(RepositoryError):
261 last = len(self.repo.revisions)
262 last = len(self.repo.revisions)
262 list(self.repo.get_changesets(start=last-1, end=last-2))
263 list(self.repo.get_changesets(start=last-1, end=last-2))
263
264
264 def test_get_changesets_numerical_id_last_zero_error(self):
265 def test_get_changesets_numerical_id_last_zero_error(self):
265 with self.assertRaises(RepositoryError):
266 with self.assertRaises(RepositoryError):
266 last = len(self.repo.revisions)
267 last = len(self.repo.revisions)
267 list(self.repo.get_changesets(start=last-1, end=0))
268 list(self.repo.get_changesets(start=last-1, end=0))
268
269
269
270
270 class ChangesetsChangesTestCaseMixin(BackendTestMixin):
271 class ChangesetsChangesTestCaseMixin(BackendTestMixin):
271 recreate_repo_per_test = False
272 recreate_repo_per_test = False
272
273
273 @classmethod
274 @classmethod
274 def _get_commits(cls):
275 def _get_commits(cls):
275 return [
276 return [
276 {
277 {
277 'message': u'Initial',
278 'message': u'Initial',
278 'author': u'Joe Doe <joe.doe@example.com>',
279 'author': u'Joe Doe <joe.doe@example.com>',
279 'date': datetime.datetime(2010, 1, 1, 20),
280 'date': datetime.datetime(2010, 1, 1, 20),
280 'added': [
281 'added': [
281 FileNode('foo/bar', content='foo'),
282 FileNode('foo/bar', content='foo'),
282 FileNode('foobar', content='foo'),
283 FileNode('foobar', content='foo'),
283 FileNode('qwe', content='foo'),
284 FileNode('qwe', content='foo'),
284 ],
285 ],
285 },
286 },
286 {
287 {
287 'message': u'Massive changes',
288 'message': u'Massive changes',
288 'author': u'Joe Doe <joe.doe@example.com>',
289 'author': u'Joe Doe <joe.doe@example.com>',
289 'date': datetime.datetime(2010, 1, 1, 22),
290 'date': datetime.datetime(2010, 1, 1, 22),
290 'added': [FileNode('fallout', content='War never changes')],
291 'added': [FileNode('fallout', content='War never changes')],
291 'changed': [
292 'changed': [
292 FileNode('foo/bar', content='baz'),
293 FileNode('foo/bar', content='baz'),
293 FileNode('foobar', content='baz'),
294 FileNode('foobar', content='baz'),
294 ],
295 ],
295 'removed': [FileNode('qwe')],
296 'removed': [FileNode('qwe')],
296 },
297 },
297 ]
298 ]
298
299
299 def test_initial_commit(self):
300 def test_initial_commit(self):
300 changeset = self.repo.get_changeset(0)
301 changeset = self.repo.get_changeset(0)
301 self.assertItemsEqual(changeset.added, [
302 self.assertItemsEqual(changeset.added, [
302 changeset.get_node('foo/bar'),
303 changeset.get_node('foo/bar'),
303 changeset.get_node('foobar'),
304 changeset.get_node('foobar'),
304 changeset.get_node('qwe'),
305 changeset.get_node('qwe'),
305 ])
306 ])
306 self.assertItemsEqual(changeset.changed, [])
307 self.assertItemsEqual(changeset.changed, [])
307 self.assertItemsEqual(changeset.removed, [])
308 self.assertItemsEqual(changeset.removed, [])
308
309
309 def test_head_added(self):
310 def test_head_added(self):
310 changeset = self.repo.get_changeset()
311 changeset = self.repo.get_changeset()
312 self.assertTrue(isinstance(changeset.added, AddedFileNodesGenerator))
311 self.assertItemsEqual(changeset.added, [
313 self.assertItemsEqual(changeset.added, [
312 changeset.get_node('fallout'),
314 changeset.get_node('fallout'),
313 ])
315 ])
316 self.assertTrue(isinstance(changeset.changed, ChangedFileNodesGenerator))
314 self.assertItemsEqual(changeset.changed, [
317 self.assertItemsEqual(changeset.changed, [
315 changeset.get_node('foo/bar'),
318 changeset.get_node('foo/bar'),
316 changeset.get_node('foobar'),
319 changeset.get_node('foobar'),
317 ])
320 ])
321 self.assertTrue(isinstance(changeset.removed, RemovedFileNodesGenerator))
318 self.assertEqual(len(changeset.removed), 1)
322 self.assertEqual(len(changeset.removed), 1)
319 self.assertEqual(list(changeset.removed)[0].path, 'qwe')
323 self.assertEqual(list(changeset.removed)[0].path, 'qwe')
320
324
321
325
322 # For each backend create test case class
326 # For each backend create test case class
323 for alias in SCM_TESTS:
327 for alias in SCM_TESTS:
324 attrs = {
328 attrs = {
325 'backend_alias': alias,
329 'backend_alias': alias,
326 }
330 }
327 # tests with additional commits
331 # tests with additional commits
328 cls_name = ''.join(('%s changesets with commits test' % alias).title().split())
332 cls_name = ''.join(('%s changesets with commits test' % alias).title().split())
329 bases = (ChangesetsWithCommitsTestCaseixin, unittest.TestCase)
333 bases = (ChangesetsWithCommitsTestCaseixin, unittest.TestCase)
330 globals()[cls_name] = type(cls_name, bases, attrs)
334 globals()[cls_name] = type(cls_name, bases, attrs)
331
335
332 # tests without additional commits
336 # tests without additional commits
333 cls_name = ''.join(('%s changesets test' % alias).title().split())
337 cls_name = ''.join(('%s changesets test' % alias).title().split())
334 bases = (ChangesetsTestCaseMixin, unittest.TestCase)
338 bases = (ChangesetsTestCaseMixin, unittest.TestCase)
335 globals()[cls_name] = type(cls_name, bases, attrs)
339 globals()[cls_name] = type(cls_name, bases, attrs)
336
340
337 # tests changes
341 # tests changes
338 cls_name = ''.join(('%s changesets changes test' % alias).title().split())
342 cls_name = ''.join(('%s changesets changes test' % alias).title().split())
339 bases = (ChangesetsChangesTestCaseMixin, unittest.TestCase)
343 bases = (ChangesetsChangesTestCaseMixin, unittest.TestCase)
340 globals()[cls_name] = type(cls_name, bases, attrs)
344 globals()[cls_name] = type(cls_name, bases, attrs)
341
345
342
346
343 if __name__ == '__main__':
347 if __name__ == '__main__':
344 unittest.main()
348 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now